diff --git a/README.md b/README.md index 40d992d..9ebfc0f 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # database-generic -Database agnostic typeclass to access generically persisted data. +Database-agnostic interface to generically persisted data. ## Introduction Explanation of the above: -- Database agnostic typeclass: the typeclass is called `MonadDb` and you must - specify how it can communicate with your database (e.g. PostgreSQL server) +- Database-agnostic interface: the interface is called `MonadDb`, and you must + specify how it can communicate with your database (e.g. PostgreSQL server). - Generically persisted data: you can derive the necessary instances for your - data types via `Generics` (or write your own instance). This will enable - `MonadDb` to read/write instances of your data types to/from your database. + data types via `Generics`. This will enable `MonadDb` to read/write instances + of your data types to/from your database. A key intended feature of this library is that the typeclass `MonadDb` can be used either server-side or client-side. Allowing your client application (e.g. -web app) to use the same functions to access data as you use server-side. +web app) to use the same functions to access your data as your server-side does. Another important intended feature is an optional `servant` server. Merely provide an instance of `MonadDb` so the server knows how to communicate with @@ -32,30 +32,33 @@ To run the tutorial on your machine: ## Features -| Feature | Status | Tested | -|------------------------------------------|--------|--------| -| Create table | ✅ | ✅ | -| Select all | ✅ | ✅ | -| Select by ID | ✅ | ✅ | -| Return subset of fields | ✅ | ✅ | -| Where column equals | | | -| Where column is null | | | -| Where column is not null | | | -| Order by clause | ✅ | ✅ | -| Order by asc/desc | | | -| Limit clause | ✅ | ✅ | -| Offset clause | ✅ | ✅ | -| Insert one | ✅ | | -| Insert many | ✅ | | -| Insert returning | ✅ | | -| Delete all | ✅ | ✅ | -| Delete by ID | ✅ | ✅ | -| Delete returning | ✅ | ✅ | -| Joins | | | -| Stream statements over Conduit | | | -| Stream updates over Conduit | | | -| Server: endpoint to execute statement | ✅ | | -| Server: stream statements over WebSocket | | | -| Server: stream updates over WebSocket | | | -| Server: permission checks | | | -| Reflex (client-side) MonadDb instance | | | +Examples of the following features can be found in [the +tutorial](tutorial/tutorial/Main.hs). + +| Feature | In Tutorial | Tested | +|------------------------------------------|-------------|--------| +| Create table | ✅ | ✅ | +| Insert one | ✅ | | +| Insert many | ✅ | | +| Insert returning | ✅ | | +| Insert returning fields | ✅ | ✅ | +| Select by PK | ✅ | ✅ | +| Select all | ✅ | ✅ | +| Select returning fields | ✅ | ✅ | +| Where column equals | | | +| Where column is null | | | +| Where column is not null | | | +| Limit clause | ✅ | ✅ | +| Offset clause | ✅ | ✅ | +| Order by clause | ✅ | ✅ | +| Delete by PK | ✅ | ✅ | +| Delete all | ✅ | ✅ | +| Delete returning | ✅ | ✅ | +| Server: endpoint to execute statement | ✅ | | +| Joins | | | +| Stream statements over Conduit | | | +| Stream updates over Conduit | | | +| Server: stream statements over WebSocket | | | +| Server: stream updates over WebSocket | | | +| Server: permission checks | | | +| Reflex (client-side) MonadDb instance | | | diff --git a/database-generic/database-generic.cabal b/database-generic/database-generic.cabal index 1df91fd..1f919f1 100644 --- a/database-generic/database-generic.cabal +++ b/database-generic/database-generic.cabal @@ -37,6 +37,7 @@ library Database.Generic.Statement.Insert, Database.Generic.Statement.Limit, Database.Generic.Statement.NoType, + Database.Generic.Statement.Order, Database.Generic.Statement.OrderBy, Database.Generic.Statement.Output, Database.Generic.Statement.Returning, diff --git a/database-generic/src/Database/Generic.hs b/database-generic/src/Database/Generic.hs index 8ac5f62..7b5a308 100644 --- a/database-generic/src/Database/Generic.hs +++ b/database-generic/src/Database/Generic.hs @@ -6,9 +6,10 @@ import Database.Generic.Class as X (MonadDb(..), MonadDbNewConn(..)) import Database.Generic.Operations as X (execute, executeTx, tx, tx_) import Database.Generic.Statement.CreateTable as X (createTable) import Database.Generic.Statement.Delete as X (deleteAll, deleteById) -import Database.Generic.Statement.Fields as X (field, field2, field3) +import Database.Generic.Statement.Fields as X ((/\), field, order) import Database.Generic.Statement.Insert as X (insertMany, insertOne) import Database.Generic.Statement.Limit as X (limit, limitOffset) +import Database.Generic.Statement.Order as X (Order(..)) import Database.Generic.Statement.OrderBy as X (orderBy) import Database.Generic.Statement.Returning as X ((==>), returning, returningFields) import Database.Generic.Statement.Select as X (selectAll, selectById) diff --git a/database-generic/src/Database/Generic/Class.hs b/database-generic/src/Database/Generic/Class.hs index 5750d69..3eb6e0a 100644 --- a/database-generic/src/Database/Generic/Class.hs +++ b/database-generic/src/Database/Generic/Class.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE UndecidableInstances #-} - module Database.Generic.Class where import Database.Generic.Database (Database, DbV) @@ -8,6 +6,7 @@ import Database.Generic.Statement (Statement) import Database.Generic.Statement.NoType qualified as NT import Database.Generic.Statement.Output (HasOutputType, Output, OutputError, OutputType, outputType) +-- | Type synonym to simplify the type of values returned by executing statements. type ExecuteReturns m db a = Either (ExecuteError (Error m db) (DbV db)) a -- | Monads that can execute database statements. @@ -21,15 +20,21 @@ class ) => MonadDb m db | m -> db where + -- | Connection type, over which statements are executed. type C m db :: Type + -- | Wrapper type over input and output to 'executeStatement'. type T m db :: Type -> Type type T m db = Identity -- Default for convenience. type Error m db :: Type type Error m db = SomeException -- Default for convenience. - -- | Execute a statement and parse the output based on expected 'OutputType'. + -- | Execute a statement and parse the 'Output' based on expected 'OutputType'. + -- + -- If you want to modify the type of value returned by 'executeStatement' (to + -- be parsed into fields of your data type), then implement 'Database' with a + -- custom 'DbV' type. Probably you don't want to do this though. executeStatement :: C m db -> (T m db) (NT.Statement, OutputType) diff --git a/database-generic/src/Database/Generic/Database.hs b/database-generic/src/Database/Generic/Database.hs index 625c8f3..b60e139 100644 --- a/database-generic/src/Database/Generic/Database.hs +++ b/database-generic/src/Database/Generic/Database.hs @@ -4,6 +4,9 @@ import Database.Generic.Prelude import Database.Generic.Entity.DbTypes (DbValue) class Database db where + -- | Type representing one cell in a row, as returned from statement execution. + -- + -- Needs to be parsable via 'FromDbValues', into the fields of your data types. type DbV db :: Type data PostgreSQL diff --git a/database-generic/src/Database/Generic/Operations.hs b/database-generic/src/Database/Generic/Operations.hs index 3462d55..b314efb 100644 --- a/database-generic/src/Database/Generic/Operations.hs +++ b/database-generic/src/Database/Generic/Operations.hs @@ -8,7 +8,7 @@ import Database.Generic.Prelude import Database.Generic.Statement (Statement(..), ToStatement(..)) import Database.Generic.Statement qualified as Statement import Database.Generic.Statement.Output (HasOutputType, OutputT, ParseOutput(..)) -import Database.Generic.Statement.Type (StatementType(..), Cons) +import Database.Generic.Statement.Type (List(..), StatementType(..)) import Database.Generic.Statement.Tx qualified as Tx import Database.Generic.Transaction (Tx, runTx) @@ -30,30 +30,30 @@ execute = -- | Like 'execute' but each 'Statement' is appended with a commit statement. executeTx :: forall m r s db. ( Database db - , HasOutputType (Cons CommitTx s) + , HasOutputType (L CommitTx s) , MonadDb m db , MonadDbWithConn m (C m db) - , ParseOutput (DbV db) (Cons CommitTx s) + , ParseOutput (DbV db) (L CommitTx s) , ToStatement r , s ~ S r ) => r - -> m (ExecuteReturns m db (OutputT (Cons CommitTx s))) + -> m (ExecuteReturns m db (OutputT (L CommitTx s))) executeTx = fmap extract . withDbConn . flip executeAndParse . pure . Statement.commitTx . statement -- | Like 'executeTx' but shape of input and output is of type 't'. executeTxs :: forall m r s db. ( Database db - , HasOutputType (Cons CommitTx s) + , HasOutputType (L CommitTx s) , MonadDb m db , MonadDbNewConn m (C m db) - , ParseOutput (DbV db) (Cons CommitTx s) + , ParseOutput (DbV db) (L CommitTx s) , ToStatement r , s ~ S r ) => (T m db) r - -> m ((T m db) (ExecuteReturns m db (OutputT (Cons CommitTx s)))) + -> m ((T m db) (ExecuteReturns m db (OutputT (L CommitTx s)))) executeTxs = (newDbConn >>=) . flip executeAndParse . fmap (Statement.commitTx . statement) diff --git a/database-generic/src/Database/Generic/Statement.hs b/database-generic/src/Database/Generic/Statement.hs index 4f387f8..120e072 100644 --- a/database-generic/src/Database/Generic/Statement.hs +++ b/database-generic/src/Database/Generic/Statement.hs @@ -7,47 +7,47 @@ import Database.Generic.Statement.Insert qualified as I import Database.Generic.Statement.Output (HasOutputType(..)) import Database.Generic.Statement.Select qualified as S import Database.Generic.Statement.Tx qualified as Tx -import Database.Generic.Statement.Type (Cons, StatementType(..)) +import Database.Generic.Statement.Type (List(..), StatementType(..)) -- | One or more statements. We track a type-level list 's' of the statements, -- because the order of the statements can affect what is returned. -data Statement (s :: [StatementType]) where - StatementBeginTx :: !Tx.BeginTx -> Statement '[BeginTx] - StatementCommitTx :: !Tx.CommitTx -> Statement '[CommitTx] - StatementCreateTable :: !(C.CreateTable a) -> Statement '[CreateTable a] - StatementDelete :: !(D.Delete o r a) -> Statement '[Delete o r a] - StatementInsert :: !(I.Insert o r a) -> Statement '[Insert o r a] - StatementSelect :: !(S.Select o r a ob) -> Statement '[Select o r a ob] - Cons :: !(Statement '[s1]) -> (Statement s2) -> Statement (Cons s1 s2) +data Statement (s :: List StatementType) where + StatementBeginTx :: !Tx.BeginTx -> Statement (One BeginTx) + StatementCommitTx :: !Tx.CommitTx -> Statement (One CommitTx) + StatementCreateTable :: !(C.CreateTable a) -> Statement (One (CreateTable a)) + StatementDelete :: !(D.Delete o r a) -> Statement (One (Delete o r a)) + StatementInsert :: !(I.Insert o r a) -> Statement (One (Insert o r a)) + StatementSelect :: !(S.Select o r a ob) -> Statement (One (Select o r a ob)) + Cons :: !(Statement (One s1)) -> !(Statement s2) -> Statement (L s1 s2) instance HasOutputType r => HasOutputType (Statement r) where outputType = outputType @r -- | Typeclass to lift individual statements into 'Statement'. class ToStatement s where - type S s :: [StatementType] + type S s :: List StatementType statement :: s -> Statement (S s) instance ToStatement Tx.CommitTx where - type S Tx.CommitTx = '[CommitTx] + type S Tx.CommitTx = One CommitTx statement = StatementCommitTx instance ToStatement (C.CreateTable (a :: Type)) where - type S (C.CreateTable a) = '[CreateTable a] + type S (C.CreateTable a) = One (CreateTable a) statement = StatementCreateTable instance ToStatement (D.Delete o (r :: Maybe fs) (a :: Type)) where - type S (D.Delete o r a) = '[Delete o r a] + type S (D.Delete o r a) = One (Delete o r a) statement = StatementDelete instance ToStatement (I.Insert o (r :: Maybe fs) (a :: Type)) where - type S (I.Insert o r a) = '[Insert o r a] + type S (I.Insert o r a) = One (Insert o r a) statement = StatementInsert instance ToStatement (S.Select o (r :: Type) (a :: Type) (ob :: Bool)) where - type S (S.Select o r a ob) = '[Select o r a ob] + type S (S.Select o r a ob) = One (Select o r a ob) statement = StatementSelect -- | Append a commit statement to a 'Statement'. -commitTx :: Statement s -> Statement (Cons CommitTx s) +commitTx :: Statement s -> Statement (L CommitTx s) commitTx = Cons $ StatementCommitTx Tx.CommitTx diff --git a/database-generic/src/Database/Generic/Statement/Fields.hs b/database-generic/src/Database/Generic/Statement/Fields.hs index 3d8b97f..64c0c30 100644 --- a/database-generic/src/Database/Generic/Statement/Fields.hs +++ b/database-generic/src/Database/Generic/Statement/Fields.hs @@ -3,6 +3,8 @@ module Database.Generic.Statement.Fields where import Data.Aeson qualified as Aeson import Database.Generic.Entity.FieldName (FieldName, HasFieldName, fieldName) import Database.Generic.Prelude +import Database.Generic.Statement.Order (Order(..)) +import Database.Generic.Statement.Type (List(..)) import Database.Generic.Serialize (Serialize(..)) import Witch qualified as W @@ -16,41 +18,80 @@ instance Serialize Fields db where serialize All = "*" serialize (Some cs) = intercalate ", " $ W.from <$> cs +-- | Named fields to order by in a statement. +newtype OrderedFields = OrderedFields [(FieldName, Order)] + deriving (Aeson.FromJSON, Eq, Generic, Semigroup, Show) + +instance Serialize Order db => Serialize OrderedFields db where + serialize (OrderedFields fs) = intercalate ", " $ fs <&> \(fn, o) -> + from fn <> " " <> serialize @_ @db o + -- | Fields 'fs' of 'a' that can be parsed into a 'b'. class FieldsOf fs a b | fs -> a, fs -> b where -- | The names of the fields to be selected. fieldNames :: fs -> [FieldName] +-- | Ordered fields 'fs' of 'a'. +class OrderedFieldsOf fs a | fs -> a where + -- | The names of the fields to be selected. + orderedFieldNames :: fs -> OrderedFields + -- * field - field3 --- | Value-level representation of a field of type 'b' belonging to 'a'. -newtype Field a b = Field { name :: FieldName } deriving Generic +-- | A named field of type 'b' belonging to 'a'. +field :: forall f a b. (HasField f a b, HasFieldName f) => Field' a (One b) +field = Field $ fieldName @f -instance FieldsOf (Field a b) a b where - fieldNames fb = [fb.name] +-- | A named fields of type 'b' belonging to 'a', with ordering 'o'. +order + :: forall f (o :: Order) a b + . (HasField f a b, HasFieldName f) + => FieldOrder a (One o) +order = Field (fieldName @f) -field :: forall f a b. (HasField f a b, HasFieldName f) => Field a b -field = Field $ fieldName @f +-- | Cons fields together. +(/\) :: Field' a (One b) -> Field' a bs -> Field' a (L b bs) +(/\) = FieldCons + +-- | One or more named fields belonging to 'a'. +-- +-- Can be used to filter results to only a subset of 'a's fields. +type Field a (bs :: List Type) = Field' a bs + +-- | One or more named fields belonging to 'a', with ordering 'o'. +-- +-- This can be used to order a query of a collection of 'a's. +type FieldOrder a (os :: List Order) = Field' a os + +data Field' a x where + Field :: !FieldName -> Field' a (One b) + FieldCons :: !(Field' a (One b)) -> !(Field' a bs) -> Field' a (L b bs) + +-- TODO the following code should avoid an instance per list length + +instance FieldsOf (Field' a (One b)) a b where + fieldNames (Field fn) = [fn] + +instance FieldsOf (Field' a (L b1 (One b2))) a (b1, b2) where + fieldNames (FieldCons f fs) = fieldNames f <> fieldNames fs + +instance FieldsOf (Field' a (L b1 (L b2 (One b3)))) a (b1, b2, b3) where + fieldNames (FieldCons f fs) = fieldNames f <> fieldNames fs -newtype F2 a b c = F2 (Field a b, Field a c) +instance FieldsOf (Field' a (L b1 (L b2 (L b3 (One b4))))) a (b1, b2, b3, b4) where + fieldNames (FieldCons f fs) = fieldNames f <> fieldNames fs -instance FieldsOf (F2 a b c) a (b, c) where - fieldNames (F2 (fb, fc)) = [fb.name, fc.name] +class SingOrder o where singOrder :: Order +instance SingOrder Asc where singOrder = Asc +instance SingOrder Desc where singOrder = Desc -field2 :: forall fb fc a b c. - ( HasField fb a b, HasFieldName fb - , HasField fc a c, HasFieldName fc - ) => F2 a b c -field2 = F2 (field @fb @a @b, field @fc @a @c) +-- TODO the following code should avoid an instance per list length -newtype F3 a b c d = F3 (Field a b, Field a c, Field a d) +instance SingOrder o => OrderedFieldsOf (Field' a (One o)) a where + orderedFieldNames (Field fn) = OrderedFields [(fn, singOrder @o)] -instance FieldsOf (F3 a b c d) a (b, c, d) where - fieldNames (F3 (fb, fc, fd)) = [fb.name, fc.name, fd.name] +instance (SingOrder o1, SingOrder o2) => OrderedFieldsOf (Field' a (L o1 (One o2)) ) a where + orderedFieldNames (FieldCons (Field f1) (Field f2)) = OrderedFields [(f1, singOrder @o1), (f2, singOrder @o2)] -field3 :: forall fb fc fd a b c d. - ( HasField fb a b, HasFieldName fb - , HasField fc a c, HasFieldName fc - , HasField fd a d, HasFieldName fd - ) => F3 a b c d -field3 = F3 (field @fb @a @b, field @fc @a @c, field @fd @a @d) +instance (SingOrder o1, SingOrder o2, SingOrder o3) => OrderedFieldsOf (Field' a (L o1 (L o2 (One o3))) ) a where + orderedFieldNames (FieldCons (Field f1) (FieldCons (Field f2) (Field f3))) = OrderedFields [(f1, singOrder @o1), (f2, singOrder @o2), (f3, singOrder @o3)] diff --git a/database-generic/src/Database/Generic/Statement/NoType.hs b/database-generic/src/Database/Generic/Statement/NoType.hs index 0a57604..70b5efc 100644 --- a/database-generic/src/Database/Generic/Statement/NoType.hs +++ b/database-generic/src/Database/Generic/Statement/NoType.hs @@ -8,6 +8,7 @@ import Database.Generic.Prelude import Database.Generic.Statement qualified as S import Database.Generic.Statement.CreateTable qualified as C import Database.Generic.Statement.Delete qualified as C +import Database.Generic.Statement.Fields (OrderedFields) import Database.Generic.Statement.Insert qualified as C import Database.Generic.Statement.Select qualified as C import Database.Generic.Statement.Tx qualified as Tx @@ -42,6 +43,7 @@ instance From (S.Statement s) Statement where instance ( Serialize DbType db , Serialize DbValue db + , Serialize OrderedFields db ) => Serialize Statement db where serialize (StatementBeginTx s) = serialize @_ @db s serialize (StatementCommitTx s) = serialize @_ @db s diff --git a/database-generic/src/Database/Generic/Statement/Order.hs b/database-generic/src/Database/Generic/Statement/Order.hs new file mode 100644 index 0000000..129c132 --- /dev/null +++ b/database-generic/src/Database/Generic/Statement/Order.hs @@ -0,0 +1,13 @@ +module Database.Generic.Statement.Order where + +import Data.Aeson qualified as Aeson +import Database.Generic.Prelude +import Database.Generic.Serialize (Serialize(..)) + +data Order = Asc | Desc deriving (Eq, Generic, Show) + +instance Aeson.FromJSON Order + +instance Serialize Order db where + serialize Asc = "ASC" + serialize Desc = "DESC" diff --git a/database-generic/src/Database/Generic/Statement/OrderBy.hs b/database-generic/src/Database/Generic/Statement/OrderBy.hs index a2e0fcc..02f60fa 100644 --- a/database-generic/src/Database/Generic/Statement/OrderBy.hs +++ b/database-generic/src/Database/Generic/Statement/OrderBy.hs @@ -1,9 +1,11 @@ module Database.Generic.Statement.OrderBy where -import Database.Generic.Statement.Fields (FieldsOf) +import Database.Generic.Statement.Fields (OrderedFieldsOf) import Database.Generic.Statement.Returning (IsReturning, Row) -- | Statements with an order by clause. +-- +-- Used to determine if a limit clause may be applied. class IsOrderedBy s -- | Modify the statement type to reflect the statement has an order by clause. @@ -11,10 +13,11 @@ type ModifyOrderedBy :: forall s1 s2. s1 -> s2 type family ModifyOrderedBy s1 -- | Class of statements to which order by clauses can be added. +-- -- Statements must already be returning something, otherwise nothing to order. class IsReturning s => OrderBy s where -- | Add an order by clause to a statement. - orderBy :: forall fs a. (FieldsOf fs (Row s) a) + orderBy :: forall fs. OrderedFieldsOf fs (Row s) => fs -- ^ Fields to order by. -> s -- ^ The original statement. -> ModifyOrderedBy s -- ^ Statement now with an order by clause. diff --git a/database-generic/src/Database/Generic/Statement/Output.hs b/database-generic/src/Database/Generic/Statement/Output.hs index aac3162..21566eb 100644 --- a/database-generic/src/Database/Generic/Statement/Output.hs +++ b/database-generic/src/Database/Generic/Statement/Output.hs @@ -5,7 +5,7 @@ module Database.Generic.Statement.Output where import Data.Aeson qualified as Aeson import Database.Generic.Prelude import Database.Generic.Entity.FromDb (FromDbValues(..)) -import Database.Generic.Statement.Type (StatementType(..)) +import Database.Generic.Statement.Type (Head, List, StatementType(..)) import Database.Generic.Statement.Type.OneOrMany (OneOrMany(..)) -- | Output from executing an SQL statement. @@ -54,12 +54,7 @@ instance HasOutputType (Insert o (Just fs) a) where instance HasOutputType (Select o fs a ob) where outputType = OutputTypeRows -type Head :: forall a. [a] -> a -type family Head xs where - Head '[a] = a - Head (a:as) = a - -instance HasOutputType (Head s) => HasOutputType (s :: [StatementType]) where +instance HasOutputType (Head s) => HasOutputType (s :: List StatementType) where outputType = outputType @(Head s) data OutputError dbv @@ -155,6 +150,6 @@ instance forall fs a ob dbv. FromDbValues dbv fs => ParseOutput dbv (Select Many parse output = Left $ ExpectedMaybeOne output -- TODO this needs to be made smarter. Requires knowledge of SQL behaviour. -instance ParseOutput dbv (Head s) => ParseOutput dbv (s :: [StatementType]) where +instance ParseOutput dbv (Head s) => ParseOutput dbv (s :: List StatementType) where type OutputT s = OutputT (Head s) parse = parse @dbv @(Head s) diff --git a/database-generic/src/Database/Generic/Statement/Returning.hs b/database-generic/src/Database/Generic/Statement/Returning.hs index 1424a99..98f2949 100644 --- a/database-generic/src/Database/Generic/Statement/Returning.hs +++ b/database-generic/src/Database/Generic/Statement/Returning.hs @@ -16,7 +16,7 @@ class Returning s1 s2 | s1 -> s2 where class ReturningFields s where -- | Update a statement 's' to return fields 'fs' (parsed into 'a's). - returningFields :: forall fs a. (FieldsOf fs (Row s) a) + returningFields :: forall fs a. FieldsOf fs (Row s) a => s -- ^ The original statement. -> fs -- ^ Fields to select, parsed into 'a's. -> ModifyReturnType s a -- ^ Statement now returning 'a's. diff --git a/database-generic/src/Database/Generic/Statement/Select.hs b/database-generic/src/Database/Generic/Statement/Select.hs index 933a4ec..ba99f08 100644 --- a/database-generic/src/Database/Generic/Statement/Select.hs +++ b/database-generic/src/Database/Generic/Statement/Select.hs @@ -6,9 +6,8 @@ import Data.Aeson qualified as Aeson import Database.Generic.Entity.DbTypes (DbValue) import Database.Generic.Entity.EntityName (EntityName, HasEntityName) import Database.Generic.Entity.EntityName qualified as Entity -import Database.Generic.Entity.FieldName (FieldName) import Database.Generic.Entity.PrimaryKey as X (PrimaryKey') -import Database.Generic.Statement.Fields (Fields(..), fieldNames) +import Database.Generic.Statement.Fields (Fields(..), OrderedFields(..), fieldNames, orderedFieldNames) import Database.Generic.Statement.Limit (Limit, Limitable(..), Offset) import Database.Generic.Statement.OrderBy (IsOrderedBy, OrderBy(..), ModifyOrderedBy) import Database.Generic.Statement.Type.OneOrMany (OneOrMany(..)) @@ -31,7 +30,7 @@ data Select' = Select' , from :: !EntityName , limit :: !(Maybe Limit) , offset :: !(Maybe Offset) - , orderBy :: ![FieldName] + , orderBy :: !OrderedFields , where' :: !(Maybe Where) } deriving (Eq, Generic, Show) @@ -51,13 +50,13 @@ instance Limitable (Select Many fs a True) where limitOffsetMay l offset (Select s) = Select s { limit = Just l, offset } instance OrderBy (Select o fs a ob) where - orderBy fs (Select s) = Select s { orderBy = fieldNames fs } + orderBy fs (Select s) = Select s { orderBy = orderedFieldNames fs } instance ReturningFields (Select o a a ob) where returningFields (Select Select' {..}) fs = Select Select' { fields = Some $ fieldNames fs, .. } -instance Serialize DbValue db => Serialize Select' db where +instance (Serialize DbValue db, Serialize OrderedFields db) => Serialize Select' db where serialize s = Serialize.statement $ unwords $ catMaybes [ Just "SELECT" , Just $ serialize s.fields @@ -65,8 +64,8 @@ instance Serialize DbValue db => Serialize Select' db where , Just $ W.from s.from , s.where' <&> \w -> "WHERE " <> serialize @_ @db w , case s.orderBy of - [] -> Nothing - fields -> Just $ "ORDER BY " <> serialize (Some fields) + (OrderedFields []) -> Nothing + fields -> Just $ "ORDER BY " <> serialize @_ @db fields , s.limit <&> \l -> "LIMIT " <> show l , s.offset <&> \o -> "OFFSET " <> show o ] @@ -80,7 +79,7 @@ selectAll = Select Select' , from = Entity.entityName @a , limit = Nothing , offset = Nothing - , orderBy = [] + , orderBy = OrderedFields [] , where' = Nothing } @@ -91,6 +90,6 @@ selectById b = Select Select' , from = Entity.entityName @a , limit = Nothing , offset = Nothing - , orderBy = [] + , orderBy = OrderedFields [] , where' = Just $ idEquals @a b } diff --git a/database-generic/src/Database/Generic/Statement/Type.hs b/database-generic/src/Database/Generic/Statement/Type.hs index 2792e74..2ac98a3 100644 --- a/database-generic/src/Database/Generic/Statement/Type.hs +++ b/database-generic/src/Database/Generic/Statement/Type.hs @@ -1,7 +1,7 @@ module Database.Generic.Statement.Type where import Database.Generic.Prelude -import Database.Generic.Statement.Type.OneOrMany (OneOrMany(..)) +import Database.Generic.Statement.Type.OneOrMany (OneOrMany) -- | All the type information we have about a statement. data StatementType where @@ -12,8 +12,9 @@ data StatementType where Insert :: OneOrMany -> Maybe fs -> Type -> StatementType Select :: OneOrMany -> Type -> Type -> Bool -> StatementType --- | Add a type to the front of a list of types. -type Cons :: forall a. a -> [a] -> [a] -type family Cons xs a where - Cons a '[] = '[a] - Cons a (x:xs) = a:xs +data List a = One a | L a (List a) + +type Head :: forall a. List a -> a +type family Head xs where + Head (One a) = a + Head (L a as) = Head as diff --git a/database-generic/test/Database/Generic/Test/Statement.hs b/database-generic/test/Database/Generic/Test/Statement.hs index 1a2675b..0c41e16 100644 --- a/database-generic/test/Database/Generic/Test/Statement.hs +++ b/database-generic/test/Database/Generic/Test/Statement.hs @@ -11,7 +11,7 @@ import Database.Generic.Serialize (Serialize(..)) import Database.Generic.Serialize qualified as Serialize import Database.Generic.Statement.CreateTable (CreateTable, CreateTable'(..), CreateTableColumn(..)) import Database.Generic.Statement.Delete (Delete, Delete'(..)) -import Database.Generic.Statement.Fields (Fields(..)) +import Database.Generic.Statement.Fields (Fields(..), OrderedFields (OrderedFields)) import Database.Generic.Statement.Limit (Limit, Offset, Limitable (limitOffsetMay)) import Database.Generic.Statement.OrderBy qualified as O import Database.Generic.Statement.Select (Select, Select'(..)) @@ -91,7 +91,7 @@ deleteByIdReturning = returning $ deleteById "john" -- | This is a test that 'returningFields' modifies the type correctly. deleteByIdReturningTwoFields :: Delete One (Just (Int64, String)) Person deleteByIdReturningTwoFields = - returningFields (deleteById "john") $ field2 @"age" @"name" + returningFields (deleteById "john") $ field @"age" /\ field @"name" deleteTests :: TestTree deleteTests = testGroup "Delete statement tests" @@ -112,13 +112,14 @@ selectAllPerson = W.from Select' , from = "person" , limit = Nothing , offset = Nothing - , orderBy = [] + , orderBy = OrderedFields [] , where' = Nothing } -- | This is a test that 'O.orderBy' modifies the type correctly. selectAllPersonOrderByName :: Select Many Person Person True -selectAllPersonOrderByName = O.orderBy (field @"name") selectAll +selectAllPersonOrderByName = + O.orderBy (order @"name" @Asc) $ selectAll @Person selectByIdPerson :: Select One Person Person False selectByIdPerson = W.from Select' @@ -126,13 +127,13 @@ selectByIdPerson = W.from Select' , from = "person" , limit = Nothing , offset = Nothing - , orderBy = [] + , orderBy = OrderedFields [] , where' = Just $ Equals "name" $ DbString "John" } selectAllPersonPG :: Limit -> Maybe Offset -> String selectAllPersonPG limit offset = Serialize.statement $ unwords $ catMaybes - [ Just $ "SELECT * FROM person ORDER BY age LIMIT " <> show limit + [ Just $ "SELECT * FROM person ORDER BY age ASC LIMIT " <> show limit , offset <&> \l -> "OFFSET " <> show l ] @@ -142,6 +143,9 @@ selectTests = testGroup "Select statement tests" , testCase "selectById @Person" $ assertEqual "" selectByIdPerson $ selectById "John" , SC.testProperty "serialize limit offset" \(l, o) -> selectAllPersonPG l o == serialize @_ @PostgreSQL - (into @Select' $ - limitOffsetMay l o $ O.orderBy (field @"age") $ selectAll @Person) + (into @Select' + $ limitOffsetMay l o + $ O.orderBy (order @"age" @Asc) + $ selectAll @Person + ) ] diff --git a/tutorial/tutorial/Main.hs b/tutorial/tutorial/Main.hs index 18343af..200a89c 100644 --- a/tutorial/tutorial/Main.hs +++ b/tutorial/tutorial/Main.hs @@ -1,7 +1,6 @@ -- This tutorial uses GHC2024. {-# LANGUAGE BlockArguments #-} -{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE TypeFamilies #-} @@ -68,40 +67,45 @@ instance MonadDbNewConn AppM PSQL.Connection where main :: IO () main = do - let c = connStr "127.0.0.1" 5432 "postgres" "demo" "demo" - let john = Person 21 "John" + let c = connStr "127.0.0.1" 5432 "postgres" "demo" "demo" + let john = Person 70 "John" let info m s = do putStrLn $ "\n" <> m print =<< runAppM c (tx $ execute s) info "Create table if not exists" $ createTable @Person True + info "Delete all" $ deleteAll @Person -- Clear table before tutorial. - info "Delete All" $ returning $ deleteAll @Person + info "Insert one" $ returning $ insertOne $ john - info "Delete by ID" $ deleteById @Person "John" + info "Insert many" $ + insertMany [Person 25 "Alice", Person 25 "Bob"] - info "Insert one" $ returning $ insertOne $ john{age=55} + info "Insert many, returning" $ + returning $ insertMany [Person 26 "Charlie", Person 26 "Dee"] - info "Insert two, returning age" $ - insertMany [john{age=25, name="Bob"}, john {name = "Mary"}] ==> field @"age" - - info "Select by ID" $ selectById @Person john.name + info "Insert many, returning age" $ + insertMany [Person 27 "Enid", Person 27 "Flavio"] ==> field @"age" info "Select all" $ selectAll @Person - info "Select all, select 1 fields" $ - selectAll @Person ==> field2 @"age" @"name" + info "Select by PK" $ selectById @Person "John" + + info "Select all, returning two fields" $ + selectAll @Person ==> (field @"age" /\ field @"name") - info "Select all, order by age" $ orderBy (field @"age") $ selectAll @Person + info "Select all, order by age then name" $ + orderBy (order @"age" @Desc /\ order @"name" @Asc) $ selectAll @Person info "Select all, limit 1" $ - limit 1 $ orderBy (field @"name") $ selectAll @Person + limit 1 $ orderBy (order @"name" @Asc) $ selectAll @Person + + info "Select all, order by name, limit 1, offset 2" + $ limitOffset 1 2 $ orderBy (order @"name" @Asc) $ selectAll @Person - info "Select all, limit 1, offset 2" $ - limitOffset 1 2 $ orderBy (field @"name") $ selectAll @Person + info "Delete by PK" $ deleteById @Person "John" - info "Select specific fields by ID" $ - selectById @Person john.name ==> field @"age" + info "Delete all, returning" $ returning $ deleteAll @Person putStrLn "\nStarting a server which will proxy any statements" Server.run (runAppM c) 1234 Server.developmentCors