packages feed

seakale-postgresql 0.2.0.1 → 0.3.0.0

raw patch · 5 files changed

+147/−45 lines, 5 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Database.Seakale.PostgreSQL: Composite :: a -> Composite a
+ Database.Seakale.PostgreSQL: TTArray :: TypeInfo -> TypeType
+ Database.Seakale.PostgreSQL: TTComposite :: [(ByteString, TypeInfo)] -> TypeType
+ Database.Seakale.PostgreSQL: TTOther :: TypeType
+ Database.Seakale.PostgreSQL: TypeInfo :: ByteString -> TypeType -> TypeInfo
+ Database.Seakale.PostgreSQL: [fromComposite] :: Composite a -> a
+ Database.Seakale.PostgreSQL: [typeName] :: TypeInfo -> ByteString
+ Database.Seakale.PostgreSQL: [typeType] :: TypeInfo -> TypeType
+ Database.Seakale.PostgreSQL: data TypeInfo
+ Database.Seakale.PostgreSQL: data TypeType
+ Database.Seakale.PostgreSQL: instance GHC.Classes.Eq a => GHC.Classes.Eq (Database.Seakale.PostgreSQL.Composite a)
+ Database.Seakale.PostgreSQL: instance GHC.Show.Show Database.Seakale.PostgreSQL.TypeInfo
+ Database.Seakale.PostgreSQL: instance GHC.Show.Show Database.Seakale.PostgreSQL.TypeType
+ Database.Seakale.PostgreSQL: instance GHC.Show.Show a => GHC.Show.Show (Database.Seakale.PostgreSQL.Composite a)
+ Database.Seakale.PostgreSQL: newtype Composite a
+ Database.Seakale.PostgreSQL.FromRow: instance (GHC.Show.Show a, Database.Seakale.FromRow.FromRow Database.Seakale.PostgreSQL.PSQL n a) => Database.Seakale.FromRow.FromRow Database.Seakale.PostgreSQL.PSQL Database.Seakale.Types.One (Database.Seakale.PostgreSQL.Composite a)
+ Database.Seakale.PostgreSQL.ToRow: instance Database.Seakale.ToRow.ToRow Database.Seakale.PostgreSQL.PSQL n a => Database.Seakale.ToRow.ToRow Database.Seakale.PostgreSQL.PSQL Database.Seakale.Types.One (Database.Seakale.PostgreSQL.Composite a)

Files

ChangeLog.md view
@@ -1,5 +1,9 @@ # Revision history for seakale-postgresql +## 0.3.0.0 -- 2017-03-30++* Support for composite types (with Composite a)+ ## 0.2.0.1 -- 2017-03-20  * Fix ToRow instance of lists.
seakale-postgresql.cabal view
@@ -1,5 +1,5 @@ name:                  seakale-postgresql-version:               0.2.0.1+version:               0.3.0.0 synopsis:              PostgreSQL backend for Seakale description:           This package provides a way to run code written with Seakale with a PostgreSQL database. license:               BSD3@@ -14,7 +14,7 @@ source-repository head   type:                darcs   location:            http://darcs.redspline.com/seakale-  tag:                 seakale-postgresql-0.2.0.1+  tag:                 seakale-postgresql-0.3.0.0  library   ghc-options:         -Wall@@ -31,6 +31,8 @@                        RecordWildCards                        OverloadedStrings                        FlexibleContexts+                       TupleSections+                       UndecidableInstances    exposed-modules:     Database.Seakale.PostgreSQL                        Database.Seakale.PostgreSQL.FromRow
src/Database/Seakale/PostgreSQL.hs view
@@ -17,6 +17,8 @@   , runStore   , runStoreT   , HasConnection(..)+  , TypeInfo(..)+  , TypeType(..)   , PSQL(..)   , defaultPSQL   , SeakaleError(..)@@ -39,6 +41,8 @@   , Eight   , Nine   , Ten+  -- * Specific types+  , Composite(..)   ) where  import           Control.Monad.Except@@ -47,6 +51,8 @@ import           Control.Monad.State import           Control.Monad.Trans.Free +import           Data.Function+import           Data.List import           Data.Maybe import           Data.Monoid import           Data.Word@@ -116,15 +122,28 @@     put s'     return x -type TypeCache = [(Oid, BS.ByteString)]+type TypeInfoOid = (BS.ByteString, Maybe [(BS.ByteString, Oid)], Maybe Oid) +data TypeType+  = TTComposite [(BS.ByteString, TypeInfo)]+  | TTArray TypeInfo+  | TTOther+  deriving Show++data TypeInfo = TypeInfo+  { typeName :: BS.ByteString+  , typeType :: TypeType+  } deriving Show++type TypeCache = [(Oid, TypeInfoOid)]+ data PSQL = PSQL { psqlLogQueries :: Bool }  defaultPSQL :: PSQL defaultPSQL = PSQL False  instance Backend PSQL where-  type ColumnType PSQL = BS.ByteString+  type ColumnType PSQL = TypeInfo    type MonadBackend PSQL m = ( HasConnection m                              , MonadState TypeCache m@@ -204,9 +223,22 @@     Just ((n,"") : _) -> return n     _ -> throwError $ "Can't get number of rows affected for " <> req -resolveType :: MonadBackend PSQL m => Oid-            -> ExceptT BS.ByteString m BS.ByteString+resolveType :: MonadBackend PSQL m => Oid -> ExceptT BS.ByteString m TypeInfo resolveType oid = do+  (name, mAttrs, mElemOID) <- resolveTypeOid oid+  case (mAttrs, mElemOID) of+    (Nothing, Nothing) -> return $ TypeInfo name TTOther+    (Just attrs, Nothing) -> do+      attrs' <- mapM (\(aname, aoid) -> (aname,) <$> resolveType aoid) attrs+      return $ TypeInfo name $ TTComposite attrs'+    (Nothing, Just eoid) -> do+      etinfo <- resolveType eoid+      return $ TypeInfo name $ TTArray etinfo+    _ -> throwError $ "Can't resolve type " <> name++resolveTypeOid :: MonadBackend PSQL m => Oid+               -> ExceptT BS.ByteString m TypeInfoOid+resolveTypeOid oid = do   cache <- get   case lookup oid cache of     Just n  -> return n@@ -219,18 +251,57 @@  getTypes :: MonadBackend PSQL m => ExceptT BS.ByteString m TypeCache getTypes = do-  res <- exec' "SELECT oid, typname FROM pg_type"-  let _until i = takeWhile (/= i) $ iterate (+1) 0-  nrows <- liftIO $ ntuples res+    -- It uses { for separator as a hack to force the presence of double quotes+    res <- exec' "SELECT pg_type.oid, typname, typtype, typelem, typlen,\+                 \ array_agg(attnum || '{' || attname || '{' || atttypid)\+                 \ FROM pg_type LEFT JOIN pg_attribute ON attrelid = typrelid\+                 \ AND typrelid <> 0 GROUP BY oid, typname, typtype, typelem,\+                 \ typlen"+    let _until i = takeWhile (/= i) $ iterate (+1) 0+    nrows <- liftIO $ ntuples res -  forM (_until nrows) $ \row -> do-    oidBS  <- liftIO $ getvalue' res row 0-    nameBS <- liftIO $ getvalue' res row 1-    case (fmap (reads . BS.unpack) oidBS, nameBS) of-      (Just ((i,"") : _), Just name) ->-        return (Oid (fromInteger i), name)-      _ -> throwError "Can't read types from pg_type"+    forM (_until nrows) $ \row -> do+      oidBS  <- liftIO $ getvalue' res row 0+      nameBS <- liftIO $ getvalue' res row 1+      typeBS <- liftIO $ getvalue' res row 2+      elemBS <- liftIO $ getvalue' res row 3+      lenBS  <- liftIO $ getvalue' res row 4+      attBS  <- liftIO $ getvalue' res row 5 +      case ( fmap (reads . BS.unpack) oidBS, nameBS, typeBS+           , fmap (reads . BS.unpack) elemBS, lenBS, attBS ) of+        (Just ((i,"") : _), Just name, _, Just ((e,""):_), Just "-1", _)+          | e /= 0 -> return ( Oid (fromInteger i)+                             , (name, Nothing, Just (Oid (fromInteger e))) )+        (Just ((i,"") : _), Just name, Just "c", _, _, Just bs) -> do+          attrs <- case readAttrs bs of+            Nothing ->+              throwError $ "Can't read attributes of composite type: " <> bs+            Just as -> return as+          return (Oid (fromInteger i), (name, Just attrs, Nothing))+        (Just ((i,"") : _), Just name, _, _, _, _) ->+          return (Oid (fromInteger i), (name, Nothing, Nothing))+        _ -> throwError "Can't read types from pg_type"++  where+    readAttrs :: BS.ByteString -> Maybe [(BS.ByteString, Oid)]+    readAttrs bs = do+      stripped <- (BS.stripPrefix "{" >=> BS.stripSuffix "}") bs+      bsTuples <- forM (BS.split ',' stripped) $ \sub -> do+        strippedSub <- (BS.stripPrefix "\"" >=> BS.stripSuffix "\"") sub+        return $ BS.split '{' strippedSub++      tuples <- forM bsTuples $ \att -> case att of+        [attnum, attname, atttypid] ->+          case (reads (BS.unpack attnum), reads (BS.unpack atttypid)) of+            ((n,""):_, (i,""):_) ->+              Just (n :: Int, attname, Oid (fromInteger i))+            _ -> Nothing+        _ -> Nothing++      return $ map (\(_,n,i) -> (n,i)) $ sortBy (compare `on` (\(n,_,_) -> n)) $+        filter (\(n,_,_) -> n > 0) tuples+ exec' :: MonadBackend PSQL m => BS.ByteString       -> ExceptT BS.ByteString m Result exec' req = do@@ -255,3 +326,5 @@     _ -> return ()    return res++newtype Composite a = Composite { fromComposite :: a } deriving (Show, Eq)
src/Database/Seakale/PostgreSQL/FromRow.hs view
@@ -20,7 +20,7 @@  instance FromRow PSQL One UTCTime where   fromRow = pconsume `pbind` \(ColumnInfo{..}, Field{..}) ->-    case (colInfoType, fieldValue) of+    case (typeName colInfoType, fieldValue) of       ("timestamp", Just bs) ->         case parseTimeM True defaultTimeLocale "%F %T%Q" (BS.unpack bs) of           Just t  -> preturn t@@ -38,46 +38,63 @@  instance {-# OVERLAPPABLE #-} FromRow PSQL One a => FromRow PSQL One [a] where   fromRow = pconsume `pbind` \(col@ColumnInfo{..}, Field{..}) ->-    case (BS.splitAt 1 colInfoType, fieldValue) of-      (("_", typ), Just bs) ->+    case (typeType colInfoType, fieldValue) of+      (TTArray subtype, Just bs) ->         pbackend `pbind` \backend ->-        arrayParser backend (col { colInfoType = typ }) bs-      (_, Just _) -> pfail $ "invalid type for list: " ++ BS.unpack colInfoType+          let col'  = col { colInfoType = subtype }+              f mBS = parseRow fromRow backend [col'] [Field mBS]+          in either pfail preturn $ seqParser '{' '}' "array" f bs+      (_, Just _) ->+        pfail $ "invalid type for list: " ++ BS.unpack (typeName colInfoType)       (_, Nothing) -> pfail "unexpected NULL for list"  -- FIXME: What about \n for example?-arrayParser :: FromRow PSQL One a => PSQL -> ColumnInfo PSQL -> BS.ByteString-            -> RowParser PSQL Zero [a]-arrayParser backend col = either pfail preturn . go-  where-    go :: FromRow PSQL One a => BS.ByteString -> Either String [a]-    go bs = case BS.splitAt 1 bs of-      ("{", "}") -> return []-      ("{", bs') -> readValues id bs'-      _ -> Left $ "invalid array starting with " ++ show (BS.take 30 bs)+seqParser :: Char -> Char -> String -> (Maybe BS.ByteString -> Either String a)+          -> BS.ByteString -> Either String [a]+seqParser ldelim rdelim descr h fullBS = case BS.uncons fullBS of+    Just (ldelim', rdelim')+      | ldelim == ldelim' && BS.singleton rdelim == rdelim' -> return []+    Just (ldelim', bs') | ldelim == ldelim' -> readValues h id bs'+    _ -> Left $ "invalid " ++ descr ++ " starting with "+                ++ show (BS.take 30 fullBS) -    readValues :: FromRow PSQL One a => ([a] -> [a]) -> BS.ByteString-               -> Either String [a]-    readValues f bs = do+  where+    readValues :: (Maybe BS.ByteString -> Either String a) -> ([a] -> [a])+               -> BS.ByteString -> Either String [a]+    readValues f g bs = do       (valBS, bs') <- readByteString bs       let mValBS = if valBS == "NULL" then Nothing else Just valBS-      val <- parseRow fromRow backend [col] [Field mValBS]-      case BS.splitAt 1 bs' of-        (",", bs'') -> readValues (f . (val :)) bs''-        ("}", "")   -> return $! f [val]-        _ -> Left $ "invalid array around " ++ show (BS.take 30 bs')+      val <- f mValBS+      case BS.uncons bs' of+        Just (',', bs'') -> readValues f (g . (val :)) bs''+        Just (rdelim', "") | rdelim == rdelim' -> return $! g [val]+        _ -> Left $ "invalid " ++ descr ++ " around " ++ show (BS.take 30 bs')      readByteString :: BS.ByteString                    -> Either String (BS.ByteString, BS.ByteString)-    readByteString bs = case BS.splitAt 1 bs of-      ("\"", bs') -> readByteString' "" bs'-      _ -> return $ BS.span (\c -> c /= ',' && c /= '}') bs+    readByteString bs = case BS.uncons bs of+      Just ('"', bs') -> readByteString' "" bs'+      _ -> return $ BS.span (\c -> c /= ',' && c /= rdelim) bs      readByteString' :: BS.ByteString -> BS.ByteString                     -> Either String (BS.ByteString, BS.ByteString)     readByteString' acc bs =-      case fmap (BS.splitAt 1) (BS.span (\c -> c /= '"' && c /= '\\') bs) of-        (bs', ("\"", bs'')) -> return (acc <> bs', bs'')-        (bs', ("\\", bs'')) -> let (c, bs''') = BS.splitAt 1 bs''-                               in readByteString' (acc <> bs' <> c) bs'''+      case fmap BS.uncons (BS.span (\c -> c /= '"' && c /= '\\') bs) of+        (bs', Just ('"', bs'')) -> case BS.uncons bs'' of+          Just ('"', bs''') -> readByteString' (bs' <> "\"") bs'''+          _ -> return (acc <> bs', bs'')+        (bs', Just ('\\', bs'')) -> let (c, bs''') = BS.splitAt 1 bs''+                                    in readByteString' (acc <> bs' <> c) bs'''         (bs', _) -> Left $ "unreadable value around " ++ show (BS.take 30 bs')++instance (Show a, FromRow PSQL n a) => FromRow PSQL One (Composite a) where+  fromRow = pconsume `pbind` \(ColumnInfo{..}, Field{..}) ->+    case (typeType colInfoType, fieldValue) of+      (TTComposite attrs, Just bs) -> pbackend `pbind` \backend ->+        either pfail preturn $ do+          let cols = map (\(name, tinfo) -> ColumnInfo (Just name) tinfo) attrs+          fields <- seqParser '(' ')' (BS.unpack (typeName colInfoType))+                              (Right . Field) bs+          Composite <$> parseRow fromRow backend cols fields+      (_, Just _) -> pfail $ "expected composite type"+      (_, Nothing) -> pfail "unexpected NULL for composite type"
src/Database/Seakale/PostgreSQL/ToRow.hs view
@@ -44,3 +44,9 @@ trimOuterQuotes bs =   let bs' = case BS.splitAt 1 bs of { ("'", b) -> b; _ -> bs }   in case BS.unsnoc bs' of { Just (bs'', '\'') -> bs''; _ -> bs' }++instance ToRow PSQL n a => ToRow PSQL One (Composite a) where+  toRow backend =+    singleton . Just . ("'(" <>) . (<> ")'") . mconcat . intersperse ","+    . map (("\"" <>) . (<> "\"") . escapeByteString . trimOuterQuotes)+    . map (fromMaybe "NULL") . vectorToList . toRow backend . fromComposite