packages feed

ribbit 0.2.1.0 → 0.2.2.0

raw patch · 4 files changed

+273/−26 lines, 4 filesdep +timePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: time

API changes (from Hackage documentation)

+ Database.Ribbit.PostgreSQL: PsqlType :: Text -> PsqlType
+ Database.Ribbit.PostgreSQL: [unPsqlType] :: PsqlType -> Text
+ Database.Ribbit.PostgreSQL: class HasFields a
+ Database.Ribbit.PostgreSQL: class HasIsNullable a
+ Database.Ribbit.PostgreSQL: class HasPsqlType a
+ Database.Ribbit.PostgreSQL: class HasPsqlTypes a
+ Database.Ribbit.PostgreSQL: createTable :: forall proxy1 proxy2 key table m. (KnownSymbol (Name table), HasPsqlTypes (DBSchema table), HasFields (DBSchema table), HasFields key, ValidKey key (DBSchema table) ~ 'True, MonadIO m) => Connection -> proxy1 key -> proxy2 table -> m ()
+ Database.Ribbit.PostgreSQL: createTableStatement :: forall proxy1 proxy2 table key. (KnownSymbol (Name table), HasPsqlTypes (DBSchema table), HasFields (DBSchema table), HasFields key, ValidKey key (DBSchema table) ~ 'True) => proxy1 key -> proxy2 table -> Text
+ Database.Ribbit.PostgreSQL: instance (GHC.TypeLits.KnownSymbol name, Database.Ribbit.PostgreSQL.HasFields more) => Database.Ribbit.PostgreSQL.HasFields (name : more)
+ Database.Ribbit.PostgreSQL: instance Data.String.IsString Database.Ribbit.PostgreSQL.PsqlType
+ Database.Ribbit.PostgreSQL: instance Database.Ribbit.PostgreSQL.HasFields '[]
+ Database.Ribbit.PostgreSQL: instance Database.Ribbit.PostgreSQL.HasIsNullable (GHC.Maybe.Maybe a)
+ Database.Ribbit.PostgreSQL: instance Database.Ribbit.PostgreSQL.HasPsqlType Data.Text.Internal.Text
+ Database.Ribbit.PostgreSQL: instance Database.Ribbit.PostgreSQL.HasPsqlType Data.Time.Calendar.Days.Day
+ Database.Ribbit.PostgreSQL: instance Database.Ribbit.PostgreSQL.HasPsqlType GHC.Types.Int
+ Database.Ribbit.PostgreSQL: instance Database.Ribbit.PostgreSQL.HasPsqlType a => Database.Ribbit.PostgreSQL.HasPsqlType (GHC.Maybe.Maybe a)
+ Database.Ribbit.PostgreSQL: instance forall k (a :: k). Database.Ribbit.PostgreSQL.HasIsNullable a
+ Database.Ribbit.PostgreSQL: instance forall k (name :: GHC.Types.Symbol) (typ :: k). GHC.TypeLits.KnownSymbol name => Database.Ribbit.PostgreSQL.HasFields (Database.Ribbit.Field name typ)
+ Database.Ribbit.PostgreSQL: instance forall k (name :: GHC.Types.Symbol) more (typ :: k). (GHC.TypeLits.KnownSymbol name, Database.Ribbit.PostgreSQL.HasFields more) => Database.Ribbit.PostgreSQL.HasFields (Database.Ribbit.Field name typ Database.Ribbit.:> more)
+ Database.Ribbit.PostgreSQL: instance forall k1 k2 (typ :: k2) (name :: k1). (Database.Ribbit.PostgreSQL.HasIsNullable typ, Database.Ribbit.PostgreSQL.HasPsqlType typ) => Database.Ribbit.PostgreSQL.HasPsqlTypes (Database.Ribbit.Field name typ)
+ Database.Ribbit.PostgreSQL: instance forall k1 k2 (typ :: k2) more (name :: k1). (Database.Ribbit.PostgreSQL.HasIsNullable typ, Database.Ribbit.PostgreSQL.HasPsqlType typ, Database.Ribbit.PostgreSQL.HasPsqlTypes more) => Database.Ribbit.PostgreSQL.HasPsqlTypes (Database.Ribbit.Field name typ Database.Ribbit.:> more)
+ Database.Ribbit.PostgreSQL: newtype PsqlType
+ Database.Ribbit.PostgreSQL: psqlType :: HasPsqlType a => proxy a -> PsqlType
+ Database.Ribbit.PostgreSQL: type family ValidKey fields schema
- Database.Ribbit: type family ResultType query
+ Database.Ribbit: type family ValidField field schema

Files

README.md view
@@ -6,10 +6,10 @@             - [Basic @Select .. From ..](#basic-select--from-)             - [Cross product](#cross-product)             - [Conditionals](#conditionals)+            - [Limited `CREATE TABLE` support.](#limited-create-table-support)     - [Roadmap](#roadmap)-        - [More conditional operatores.](#more-conditional-operatores)-        - [`CREATE TABLE` support.](#create-table-support)         - [`INSERT INTO` support.](#insert-into-support)+        - [Flesh out Haskell to PostgreSQL type mapping.](#flesh-out-haskell-to-postgresql-type-mapping)     - [How it compares with other libraries.](#how-it-compares-with-other-libraries)     - [The name: Ribbit](#the-name-ribbit) @@ -44,9 +44,11 @@  ## Status -The status of Ribbit "Very Incomplete". My goal is to take a "depth first"-approach, where every feature added is production ready before moving on to the-next feature. Featured back-ends include `postgres-simple` at this time.+The status of Ribbit "Very Incomplete". My goal is to take a "depth+first" approach, where every feature added is production ready+before moving on to the next feature. Featured back-ends include+[postgresql-simple](https://hackage.haskell.org/package/postgresql-simple)+at this time.  ### Current Features @@ -56,19 +58,25 @@  We support queries of the form: -> type MyQuery = Select '["field1", "field2"] `From` MyTable+```haskell+type MyQuery = Select '["field1", "field2"] `From` MyTable+```  #### Cross product  We support queries of the form: +```haskell type MyQuery = Select '["t1.field1", "t2.field2"] `From` MyTable1 `As` "t1" `X` MyTable2 `As` "t2"+```  #### Conditionals  We support queries of the form: -> type MyQuery = Select '["field1", "field2"] `From` MyTable `Where` \<condition\>+```haskell+type MyQuery = Select '["field1", "field2"] `From` MyTable `Where` <condition>+```  Where ```<condition>``` can include: @@ -84,24 +92,43 @@ - ```"field" `NotEquals` (?)```: Test for inequality against a query parameter. - ```"field1" `NotEquals` "field2"```: Test for inequality between two fields. -## Roadmap+#### Limited `CREATE TABLE` support. -This is what I plan to work on next:+The postgresql-simple backend supports creating tables in the+database. This support is "limited" because it misses the following+features: -### `CREATE TABLE` support.+- PostgreSQL column types are know for only a small number of Haskell types.+  This is extensible by the user (by implementing a type class), but it would+  be nice to have a more comprehensive set of mappings out of the box. -One source of programming bugs is when the schema in the database and the-schema described by your schema types get out of sync. It is never possible to-always ensure at compile time that your database will match your program when-actually run, but the addition of `CREATE TABLE` support will at least make it-possible for the database schema to have one source of truth in your codebase-(as opposed to having to maintain Ribbit schemas and also a corresponding-schema embedded in some database initialization script somewhere).+- Foreign key constraints are not yet supported. +- Arbitrary non-primary-key indexes are not yet supported.++What *IS* supported already is:++- Determining whether a field is nullable, based on whether the corresponding+  Haskell type is wrapped in a `Maybe`.++- Compound primary keys. I.e. primary keys consisting of more than one+  component.+++## Roadmap++This is what I plan to work on next:+ ### `INSERT INTO` support.  Obviously, we want to do more with our DB than just read from it. +### Flesh out Haskell to PostgreSQL type mapping.++As mentioned above, only a small number of Haskell types are mapped+to PostgreSQL types out of the box. We would like to make this a more+comprehensive mapping out of the box.+ ## How it compares with other libraries.  The short answer is there are a lot of other libraries and I'm not sure.@@ -137,7 +164,7 @@   Then you would be free to deconstruct this type (using type families),   transform it into another schema, generate customized `CREATE TABLE`   statements if the (forthcoming) ones provided aren't good enough for your-  back-end or use case... that sort of thing. As a somewhat contrived example,+  back-end or use case... that sort of thing. As a somewhat contrived example:   maybe, for who knows what reason, you never want to allow null values in your   database. You can write a type family that can inspect every field in an   arbitrary schema, replacing all the `Maybe a` with just `a`, like:
ribbit.cabal view
@@ -1,6 +1,6 @@  name:                ribbit-version:             0.2.1.0+version:             0.2.2.0 synopsis:            ribbit -- description:          homepage:            https://github.com/owensmurray/ribbit@@ -24,7 +24,8 @@     Only              >= 0.1     && < 0.2,     base              >= 4.12    && < 4.13,     postgresql-simple >= 0.6.2   && < 0.7,-    text              >= 1.2.3.1 && < 1.3+    text              >= 1.2.3.1 && < 1.3,+    time              >= 1.8.0.2 && < 1.9   hs-source-dirs:      src   default-language:    Haskell2010 
src/Database/Ribbit.hs view
@@ -56,7 +56,8 @@   -- * Transformations on Query Types   ArgsType,   ResultType,-  +  ValidField,+   -- * Query Rendering   Render(..) @@ -344,7 +345,8 @@  {- |   Helper for 'ArgsType'. Reduces the number of equations required, because-  'ArgsType' doesn't actually care about which condition it is inspecting.+  'ArgsType' doesn't actually care about which conditionl operator it+  is inspecting. -} data Condition l r @@ -359,7 +361,7 @@     )  -{- | Produce a type error if the field is not contained within the schema. -}+{- | Type level check to see if the field is actually contained in the schema -} type family ValidField field schema where   ValidField name (Field name typ) = 'True   ValidField name (Field _ typ) = 'False
src/Database/Ribbit/PostgreSQL.hs view
@@ -1,26 +1,63 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}  {- | "postgresql-simple"-backed query ribbit implementation. -} module Database.Ribbit.PostgreSQL (+  -- * Integrating your types.+  HasPsqlType(..),+  PsqlType(..),++  -- * Performing queries.   query,++  -- * Creating tables.+  createTable,+  createTableStatement,++  -- * Other types.+  -- | These type classes/families are not meant to be used directly. They+  -- are exported primarily because they appear in the type signatures+  -- of some of the above functions and documenting them can be helpful+  -- when trying to figure out how to use those functions.+  HasFields,+  HasPsqlTypes,+  HasIsNullable,+  ValidKey,   FromRow,   ToRow, ) where  +import Control.Monad (void) import Control.Monad.IO.Class (MonadIO, liftIO)-import Data.Proxy (Proxy)-import Data.String (fromString)+import Data.Proxy (Proxy(Proxy))+import Data.String (fromString, IsString)+import Data.Text (Text)+import Data.Time (Day) import Data.Tuple.Only (Only(Only))+import Data.Type.Bool (If) import Database.PostgreSQL.Simple (Connection) import Database.PostgreSQL.Simple.FromField (FromField) import Database.PostgreSQL.Simple.ToField (Action, ToField)-import Database.Ribbit (Render, render, ArgsType, ResultType, (:>)((:>)))+import Database.Ribbit (Render, render, ArgsType, ResultType, (:>)((:>)),+  Name, Field, DBSchema, ValidField)+import GHC.TypeLits (KnownSymbol, TypeError, ErrorMessage((:<>:),+  ShowType)) import qualified Data.Text as T import qualified Database.PostgreSQL.Simple as PG import qualified Database.PostgreSQL.Simple.FromRow as PGF import qualified Database.PostgreSQL.Simple.ToRow as PGT+import qualified GHC.TypeLits as Lit   {- | Execute a query against a PostgreSQL database connection. -}@@ -42,6 +79,163 @@       (Wrap args)  +{- |+  Create the indicated table in the database.++  See 'createTableStatement' for details.+-}+createTable :: forall proxy1 proxy2 key table m. (+    KnownSymbol (Name table),+    HasPsqlTypes (DBSchema table),+    HasFields (DBSchema table),+    HasFields key,+    ValidKey key (DBSchema table) ~ 'True,+    MonadIO m+  )+  => Connection+  -> proxy1 key+  -> proxy2 table+  -> m ()+createTable conn key table =+  let+    stmt :: Text+    stmt = createTableStatement key table+  in+    liftIO . void $+      PG.execute_+        conn+        (fromString . T.unpack $ stmt)+++{- |+  Produce the statement used to create a table.++  In this example, we create an employee table with a multi-part primary+  key, one nullable field, and a few non-nullable fields.++  > data Employee+  > instance Table Employee where+  >   type Name = "employees"+  >   type DBSchema =+  >     Field "company_id" Int+  >     :> Field "id" Int+  >     :> Field "name" Text+  >     :> Field "quit_date" (Maybe Day)+  >+  > let+  >   primaryKey :: Proxy '["company_id", "id"]+  >   primaryKey = Proxy+  >   +  >   table :: Proxy Employee+  >   table = Proxy+  >+  > in+  >   createTableStatement primaryKey table++  This will produce the statement:++  > "create table employees (company_id integer not null, id integer not null, name text not null, quit_date date, primary key (company_id, id));"+-}+createTableStatement :: forall proxy1 proxy2 table key. (+    KnownSymbol (Name table),+    HasPsqlTypes (DBSchema table),+    HasFields (DBSchema table),+    HasFields key,+    ValidKey key (DBSchema table) ~ 'True+  )+  => proxy1 key+  -> proxy2 table+  -> Text+createTableStatement key _table =+    "create table " <> symbolVal tableName+    <> " (" <> T.intercalate ", " [+      field+      <> " "+      <> typ+      | (field, typ) <- zip (fields schema) (psqlTypes schema)+    ]+    <> (+      case fields key of+        [] -> ""+        fs -> ", primary key (" <> T.intercalate ", " fs <> ")"+      )+    <> ");"+  where+    schema :: Proxy (DBSchema table)+    schema = Proxy++    tableName :: Proxy (Name table)+    tableName = Proxy+++class HasFields a where+  fields :: proxy a -> [Text]+instance (KnownSymbol name) => HasFields (Field name typ) where+  fields _proxy = [symbolVal (Proxy @name)]+instance (KnownSymbol name, HasFields more) =>+    HasFields (Field name typ :> more)+  where+    fields _proxy = symbolVal (Proxy @name) : fields (Proxy @more)+instance HasFields '[] where+  fields _proxy = []+instance (KnownSymbol name, HasFields more) => HasFields (name:more) where+  fields _proxy = symbolVal (Proxy @name) : fields (Proxy @more)+++class HasPsqlTypes a where+  psqlTypes :: proxy a -> [Text]+instance (HasIsNullable typ, HasPsqlType typ) => HasPsqlTypes (Field name typ) where+  psqlTypes _proxy =+    [+      unPsqlType (psqlType (Proxy @typ))+      <> if isNullable (Proxy @typ) then "" else " not null"+    ]+instance (HasIsNullable typ, HasPsqlType typ, HasPsqlTypes more) =>+    HasPsqlTypes (Field name typ :> more)+  where+    psqlTypes _proxy =+      psqlTypes (Proxy @(Field name typ)) ++ psqlTypes (Proxy @more)+++{- |+  Given a Haskell type, produce the PostgreSQL type of columns that+  store values of that haskell type.+-}+class HasPsqlType a where+  psqlType :: proxy a -> PsqlType+instance (HasPsqlType a) => HasPsqlType (Maybe a) where+  psqlType _proxy = psqlType (Proxy @a)+instance HasPsqlType Text where+  psqlType _proxy = "text"+instance HasPsqlType Int where+  psqlType _proxy = "integer"+instance HasPsqlType Day where+  psqlType _proxy = "date"+++class HasIsNullable a where+  isNullable :: proxy a -> Bool+instance HasIsNullable (Maybe a) where+  isNullable _proxy = True+instance {-# OVERLAPPABLE #-} HasIsNullable a where+  isNullable _proxy = False+++{- |+  Represents the "base" PostgreSQL type. We say "base" type because+  whether the type is nullable is handle automatically.++  e.g.++  * @PsqlType "integer"@+  * @PsqlType "timestamp with time zone"@+-}+newtype PsqlType = PsqlType {+    unPsqlType :: Text+  }+  deriving newtype (IsString)++ {- | Like 'PGF.FromRow', but defined here so we can avoid orphaned instances. -} class FromRow a where   fromRow :: PGF.RowParser a@@ -74,5 +268,28 @@   fromRow = Wrap <$> fromRow instance (ToRow a) => PGT.ToRow (Wrap a) where   toRow = toRow . unWrap+++{- | Like 'Lit.symbolVal', but produce any kind of string-like thing. -}+symbolVal :: (KnownSymbol n, IsString a) => proxy n -> a+symbolVal = fromString . Lit.symbolVal+++{- | Make sure the proposed primary key is legit. -}+type family ValidKey fields schema where+  ValidKey '[] schema = 'True+  ValidKey (field:more) schema =+    If+      (ValidField field schema)+      (ValidKey more schema)+      (+        TypeError (+          'Lit.Text "field "+          ':<>: 'ShowType field+          ':<>: 'Lit.Text " is not part of the schema, so it cannot be\+                          \ used as a component of the primary key."+        )+      )+