{-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances, DefaultSignatures #-}
module Database.Selda.SqlType
( SqlType (..), SqlEnum (..)
, Lit (..), UUID, RowID, ID, SqlValue (..), SqlTypeRep (..)
, invalidRowId, isInvalidRowId, toRowId, fromRowId
, fromId, toId, invalidId, isInvalidId, untyped
, compLit, litType
, sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat
) where
import Control.Applicative ((<|>))
import Data.ByteString (ByteString, empty)
import qualified Data.ByteString.Lazy as BSL
import Data.Maybe (fromJust)
import Data.Proxy
import Data.Text (Text, pack, unpack)
import Data.Time
import Data.Typeable
import Data.UUID.Types (UUID, toString, fromByteString, nil)
sqlDateTimeFormat :: String
sqlDateTimeFormat = "%F %H:%M:%S%Q%z"
sqlDateFormat :: String
sqlDateFormat = "%F"
sqlTimeFormat :: String
sqlTimeFormat = "%H:%M:%S%Q%z"
data SqlTypeRep
= TText
| TRowID
| TInt
| TFloat
| TBool
| TDateTime
| TDate
| TTime
| TBlob
| TUUID
| TJSON
deriving (Show, Eq, Ord)
class Typeable a => SqlType a where
mkLit :: a -> Lit a
default mkLit :: (Typeable a, SqlEnum a) => a -> Lit a
mkLit = LCustom TText . LText . toText
sqlType :: Proxy a -> SqlTypeRep
sqlType _ = litType (defaultValue :: Lit a)
fromSql :: SqlValue -> a
default fromSql :: (Typeable a, SqlEnum a) => SqlValue -> a
fromSql = fromText . fromSql
defaultValue :: Lit a
default defaultValue :: (Typeable a, SqlEnum a) => Lit a
defaultValue = mkLit (minBound :: a)
class (Typeable a, Bounded a, Enum a) => SqlEnum a where
toText :: a -> Text
fromText :: Text -> a
instance {-# OVERLAPPABLE #-}
(Typeable a, Bounded a, Enum a, Show a, Read a) => SqlEnum a where
toText = pack . show
fromText = read . unpack
data Lit a where
LText :: !Text -> Lit Text
LInt :: !Int -> Lit Int
LDouble :: !Double -> Lit Double
LBool :: !Bool -> Lit Bool
LDateTime :: !UTCTime -> Lit UTCTime
LDate :: !Day -> Lit Day
LTime :: !TimeOfDay -> Lit TimeOfDay
LJust :: SqlType a => !(Lit a) -> Lit (Maybe a)
LBlob :: !ByteString -> Lit ByteString
LNull :: SqlType a => Lit (Maybe a)
LCustom :: SqlTypeRep -> Lit a -> Lit b
LUUID :: !UUID -> Lit UUID
litType :: Lit a -> SqlTypeRep
litType (LText{}) = TText
litType (LInt{}) = TInt
litType (LDouble{}) = TFloat
litType (LBool{}) = TBool
litType (LDateTime{}) = TDateTime
litType (LDate{}) = TDate
litType (LTime{}) = TTime
litType (LJust x) = litType x
litType (LBlob{}) = TBlob
litType (x@LNull) = sqlType (proxyFor x)
where
proxyFor :: Lit (Maybe a) -> Proxy a
proxyFor _ = Proxy
litType (LCustom t _) = t
litType (LUUID{}) = TUUID
instance Eq (Lit a) where
a == b = compLit a b == EQ
instance Ord (Lit a) where
compare = compLit
litConTag :: Lit a -> Int
litConTag (LText{}) = 0
litConTag (LInt{}) = 1
litConTag (LDouble{}) = 2
litConTag (LBool{}) = 3
litConTag (LDateTime{}) = 4
litConTag (LDate{}) = 5
litConTag (LTime{}) = 6
litConTag (LJust{}) = 7
litConTag (LBlob{}) = 8
litConTag (LNull) = 9
litConTag (LCustom{}) = 10
litConTag (LUUID{}) = 11
compLit :: Lit a -> Lit b -> Ordering
compLit (LText x) (LText x') = x `compare` x'
compLit (LInt x) (LInt x') = x `compare` x'
compLit (LDouble x) (LDouble x') = x `compare` x'
compLit (LBool x) (LBool x') = x `compare` x'
compLit (LDateTime x) (LDateTime x') = x `compare` x'
compLit (LDate x) (LDate x') = x `compare` x'
compLit (LTime x) (LTime x') = x `compare` x'
compLit (LBlob x) (LBlob x') = x `compare` x'
compLit (LJust x) (LJust x') = x `compLit` x'
compLit (LCustom _ x) (LCustom _ x') = x `compLit` x'
compLit (LUUID x) (LUUID x') = x `compare` x'
compLit a b = litConTag a `compare` litConTag b
data SqlValue where
SqlInt :: !Int -> SqlValue
SqlFloat :: !Double -> SqlValue
SqlString :: !Text -> SqlValue
SqlBool :: !Bool -> SqlValue
SqlBlob :: !ByteString -> SqlValue
SqlUTCTime :: !UTCTime -> SqlValue
SqlTime :: !TimeOfDay -> SqlValue
SqlDate :: !Day -> SqlValue
SqlNull :: SqlValue
instance Show SqlValue where
show (SqlInt n) = "SqlInt " ++ show n
show (SqlFloat f) = "SqlFloat " ++ show f
show (SqlString s) = "SqlString " ++ show s
show (SqlBool b) = "SqlBool " ++ show b
show (SqlBlob b) = "SqlBlob " ++ show b
show (SqlUTCTime t) = "SqlUTCTime " ++ show t
show (SqlTime t) = "SqlTime " ++ show t
show (SqlDate d) = "SqlDate " ++ show d
show (SqlNull) = "SqlNull"
instance Show (Lit a) where
show (LText s) = show s
show (LInt i) = show i
show (LDouble d) = show d
show (LBool b) = show b
show (LDateTime s) = show s
show (LDate s) = show s
show (LTime s) = show s
show (LBlob b) = show b
show (LJust x) = "Just " ++ show x
show (LNull) = "Nothing"
show (LCustom _ l) = show l
show (LUUID u) = toString u
newtype RowID = RowID Int
deriving (Eq, Ord, Typeable)
instance Show RowID where
show (RowID n) = show n
invalidRowId :: RowID
invalidRowId = RowID (-1)
isInvalidRowId :: RowID -> Bool
isInvalidRowId (RowID n) = n < 0
toRowId :: Int -> RowID
toRowId = RowID
fromRowId :: RowID -> Int
fromRowId (RowID n) = n
newtype ID a = ID {untyped :: RowID}
deriving (Eq, Ord, Typeable)
instance Show (ID a) where
show = show . untyped
toId :: Int -> ID a
toId = ID . toRowId
fromId :: ID a -> Int
fromId (ID i) = fromRowId i
invalidId :: ID a
invalidId = ID invalidRowId
isInvalidId :: ID a -> Bool
isInvalidId = isInvalidRowId . untyped
instance SqlType RowID where
mkLit (RowID n) = LCustom TRowID (LInt n)
sqlType _ = TRowID
fromSql (SqlInt x) = RowID x
fromSql v = error $ "fromSql: RowID column with non-int value: " ++ show v
defaultValue = mkLit invalidRowId
instance Typeable a => SqlType (ID a) where
mkLit (ID n) = LCustom TRowID (mkLit n)
sqlType _ = TRowID
fromSql = ID . fromSql
defaultValue = mkLit (ID invalidRowId)
instance SqlType Int where
mkLit = LInt
sqlType _ = TInt
fromSql (SqlInt x) = x
fromSql v = error $ "fromSql: int column with non-int value: " ++ show v
defaultValue = LInt 0
instance SqlType Double where
mkLit = LDouble
sqlType _ = TFloat
fromSql (SqlFloat x) = x
fromSql v = error $ "fromSql: float column with non-float value: " ++ show v
defaultValue = LDouble 0
instance SqlType Text where
mkLit = LText
sqlType _ = TText
fromSql (SqlString x) = x
fromSql v = error $ "fromSql: text column with non-text value: " ++ show v
defaultValue = LText ""
instance SqlType Bool where
mkLit = LBool
sqlType _ = TBool
fromSql (SqlBool x) = x
fromSql (SqlInt 0) = False
fromSql (SqlInt _) = True
fromSql v = error $ "fromSql: bool column with non-bool value: " ++ show v
defaultValue = LBool False
instance SqlType UTCTime where
mkLit = LDateTime
sqlType _ = TDateTime
fromSql (SqlUTCTime t) = t
fromSql (SqlString s) =
case withWeirdTimeZone sqlDateTimeFormat (unpack s) of
Just t -> t
_ -> error $ "fromSql: bad datetime string: " ++ unpack s
fromSql v = error $ "fromSql: datetime column with non-datetime value: " ++ show v
defaultValue = LDateTime $ UTCTime (ModifiedJulianDay 40587) 0
instance SqlType Day where
mkLit = LDate
sqlType _ = TDate
fromSql (SqlDate d) = d
fromSql (SqlString s) =
case parseTimeM True defaultTimeLocale sqlDateFormat (unpack s) of
Just t -> t
_ -> error $ "fromSql: bad date string: " ++ unpack s
fromSql v = error $ "fromSql: date column with non-date value: " ++ show v
defaultValue = LDate $ ModifiedJulianDay 40587
instance SqlType TimeOfDay where
mkLit = LTime
sqlType _ = TTime
fromSql (SqlTime s) = s
fromSql (SqlString s) =
case withWeirdTimeZone sqlTimeFormat (unpack s) of
Just t -> t
_ -> error $ "fromSql: bad time string: " ++ unpack s
fromSql v = error $ "fromSql: time column with non-time value: " ++ show v
defaultValue = LTime $ TimeOfDay 0 0 0
withWeirdTimeZone :: ParseTime t => String -> String -> Maybe t
withWeirdTimeZone fmt s =
parseTimeM True defaultTimeLocale fmt (s++"00")
<|> parseTimeM True defaultTimeLocale fmt s
<|> parseTimeM True defaultTimeLocale fmt (s++"+0000")
instance SqlType ByteString where
mkLit = LBlob
sqlType _ = TBlob
fromSql (SqlBlob x) = x
fromSql v = error $ "fromSql: blob column with non-blob value: " ++ show v
defaultValue = LBlob empty
instance SqlType BSL.ByteString where
mkLit = LCustom TBlob . LBlob . BSL.toStrict
sqlType _ = TBlob
fromSql (SqlBlob x) = BSL.fromStrict x
fromSql v = error $ "fromSql: blob column with non-blob value: " ++ show v
defaultValue = LCustom TBlob (LBlob empty)
instance SqlType UUID where
mkLit = LUUID
sqlType _ = TUUID
fromSql (SqlBlob x) = fromJust . fromByteString $ BSL.fromStrict x
fromSql v = error $ "fromSql: UUID column with non-blob value: " ++ show v
defaultValue = LUUID nil
instance SqlType a => SqlType (Maybe a) where
mkLit (Just x) = LJust $ mkLit x
mkLit Nothing = LNull
sqlType _ = sqlType (Proxy :: Proxy a)
fromSql (SqlNull) = Nothing
fromSql x = Just $ fromSql x
defaultValue = LNull
instance SqlType Ordering