mirror of https://github.com/voidlizard/hbs2
196 lines
6.2 KiB
Haskell
196 lines
6.2 KiB
Haskell
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||
{-# LANGUAGE UndecidableInstances #-}
|
||
{-# LANGUAGE DefaultSignatures #-}
|
||
module DBPipe.SQLite.Generic where
|
||
|
||
import DBPipe.SQLite.Types
|
||
import DBPipe.SQLite qualified as SQL
|
||
import DBPipe.SQLite hiding (insert,columnName)
|
||
|
||
import GHC.Generics
|
||
import Data.Proxy
|
||
import Data.Text qualified as Text
|
||
import Data.Text (Text)
|
||
import Data.String (IsString(..))
|
||
import Text.InterpolatedString.Perl6 (qc)
|
||
import Data.Coerce
|
||
import UnliftIO
|
||
|
||
newtype SQLName = SQLName Text
|
||
deriving stock (Eq,Ord)
|
||
deriving newtype (IsString,Monoid,Semigroup,Show)
|
||
|
||
newtype SQLPart = SQLPart { fromSQL :: Text }
|
||
deriving stock (Eq,Ord)
|
||
deriving newtype (IsString,Monoid,Semigroup,Show)
|
||
|
||
data AllColumns a = AllColumns
|
||
deriving stock (Generic)
|
||
|
||
class ToSQL a where
|
||
toSQL :: a -> SQLPart
|
||
|
||
instance ToSQL SQLName where
|
||
toSQL (SQLName a) = SQLPart a
|
||
|
||
|
||
class GHasColumnNames f where
|
||
gColumnNames :: f p -> [SQLName]
|
||
|
||
class HasTableName a where
|
||
tableName :: SQLName
|
||
|
||
class HasColumnNames a where
|
||
columnNames :: a -> [SQLName]
|
||
default columnNames :: (Generic a, GHasColumnNames (Rep a)) => a -> [SQLName]
|
||
columnNames = gColumnNames . from
|
||
|
||
class HasColumnName a where
|
||
columnName :: SQLName
|
||
|
||
instance HasColumnNames [SQLName] where
|
||
columnNames = id
|
||
|
||
instance HasColumnNames SQLName where
|
||
columnNames n = [n]
|
||
|
||
instance {-# OVERLAPPABLE #-} (Generic a, GHasColumnNames (Rep a)) => HasColumnNames a
|
||
|
||
instance GHasColumnNames U1 where
|
||
gColumnNames U1 = []
|
||
|
||
instance (GHasColumnNames a, GHasColumnNames b) => GHasColumnNames (a :*: b) where
|
||
gColumnNames (a :*: b) = gColumnNames a <> gColumnNames b
|
||
|
||
instance (GHasColumnNames a, GHasColumnNames b) => GHasColumnNames (a :+: b) where
|
||
gColumnNames _ = [] -- Не используется для нашего случая.
|
||
|
||
instance HasColumnName c => GHasColumnNames (K1 i c) where
|
||
gColumnNames (K1 c) = [columnName @c]
|
||
|
||
instance GHasColumnNames a => GHasColumnNames (M1 i t a) where
|
||
gColumnNames (M1 a) = gColumnNames a
|
||
|
||
|
||
class GColumnNames f where
|
||
gColumnNames1 :: [SQLName]
|
||
|
||
instance GColumnNames U1 where
|
||
gColumnNames1 = []
|
||
|
||
instance (GColumnNames a, GColumnNames b) => GColumnNames (a :+: b) where
|
||
gColumnNames1 = gColumnNames1 @a ++ gColumnNames1 @b
|
||
|
||
instance (GColumnNames a, GColumnNames b) => GColumnNames (a :*: b) where
|
||
gColumnNames1 = gColumnNames1 @a ++ gColumnNames1 @b
|
||
|
||
instance (Selector s, HasColumnName c) => GColumnNames (M1 S s (K1 i c)) where
|
||
gColumnNames1 = [columnName @c]
|
||
|
||
instance GColumnNames a => GColumnNames (M1 D d a) where
|
||
gColumnNames1 = gColumnNames1 @a
|
||
|
||
instance GColumnNames a => GColumnNames (M1 C c a) where
|
||
gColumnNames1 = gColumnNames1 @a
|
||
|
||
instance (Generic a, GColumnNames (Rep a)) => HasColumnNames (AllColumns a) where
|
||
columnNames _ = gColumnNames1 @(Rep a)
|
||
|
||
-- -- Реализация GHasColumnNames для AllColumns a
|
||
-- instance (Generic a, GHasColumnNames (Rep a)) => GHasColumnNames AllColumns where
|
||
-- gColumnNames _ = gColumnNames (from (undefined :: a))
|
||
|
||
-- -- Функция для получения списка имен колонок через AllColumns
|
||
-- columnNamesForAll :: forall a. (Generic a, GHasColumnNames AllColumns) => [SQLName]
|
||
-- columnNamesForAll = gColumnNames (AllColumns @a)
|
||
|
||
-- Пример использования этой функции:
|
||
-- myList = columnNamesFor (Proxy :: Proxy GitRepoListEntry)
|
||
|
||
data Bound = forall a . ToField a => Bound a
|
||
|
||
class GToBoundList f where
|
||
gToBoundList :: f p -> [Bound]
|
||
|
||
instance GToBoundList U1 where
|
||
gToBoundList U1 = []
|
||
|
||
instance (GToBoundList a, GToBoundList b) => GToBoundList (a :*: b) where
|
||
gToBoundList (a :*: b) = gToBoundList a <> gToBoundList b
|
||
|
||
instance (ToField c) => GToBoundList (K1 i c) where
|
||
gToBoundList (K1 c) = [Bound c]
|
||
|
||
instance GToBoundList a => GToBoundList (M1 i t a) where
|
||
gToBoundList (M1 a) = gToBoundList a
|
||
|
||
class ToBoundList a where
|
||
toBoundList :: a -> [Bound]
|
||
default toBoundList :: (Generic a, GToBoundList (Rep a)) => a -> [Bound]
|
||
toBoundList = gToBoundList . from
|
||
|
||
instance (Generic a, GToBoundList (Rep a)) => ToBoundList a where
|
||
toBoundList = gToBoundList . from
|
||
|
||
columnListPart :: forall a . HasColumnNames a => a -> SQLPart
|
||
columnListPart w = SQLPart $ Text.intercalate "," [ coerce @_ @Text x | x <- columnNames w ]
|
||
|
||
bindListPart :: forall a . HasColumnNames a => a -> SQLPart
|
||
bindListPart w = SQLPart $ Text.intercalate "," [ "?" | _ <- columnNames w ]
|
||
|
||
class HasPrimaryKey t where
|
||
primaryKey :: [SQLName]
|
||
|
||
newtype OnCoflictIgnore t r = OnCoflictIgnore r
|
||
deriving stock (Generic)
|
||
|
||
instance (HasPrimaryKey t, HasColumnNames r) => HasColumnNames (OnCoflictIgnore t r) where
|
||
columnNames (OnCoflictIgnore r) = columnNames r
|
||
|
||
-- instance (HasColumnNames r) => HasColumnNames (AllColumns r) where
|
||
-- columnNames _ = gColumnNames @r
|
||
-- columnNames AllColumns = columnNames r
|
||
|
||
onConflictIgnore :: (HasTableName t, HasColumnNames r) => r -> OnCoflictIgnore t r
|
||
onConflictIgnore = OnCoflictIgnore
|
||
|
||
instance ToField Bound where
|
||
toField (Bound x) = toField x
|
||
|
||
data BoundQuery =
|
||
BoundQuery SQLPart [Bound]
|
||
|
||
class (MonadIO m, HasTableName t, HasColumnNames b) => Insert t b m where
|
||
insert :: b -> DBPipeM m ()
|
||
|
||
instance {-# OVERLAPPABLE #-}
|
||
( MonadIO m
|
||
, HasTableName t
|
||
, HasColumnNames b
|
||
, ToBoundList b
|
||
) => Insert t b m where
|
||
insert values = do
|
||
SQL.insert [qc|insert into {tn} values({v}) ({n})|] bound
|
||
where
|
||
v = coerce @_ @Text $ bindListPart values
|
||
n = coerce @_ @Text $ columnListPart values
|
||
bound = toBoundList values
|
||
tn = coerce @_ @Text (tableName @t)
|
||
|
||
instance {-# OVERLAPPABLE #-}
|
||
( MonadIO m
|
||
, HasTableName t
|
||
, HasPrimaryKey t
|
||
, HasColumnNames b
|
||
, ToBoundList b
|
||
) => Insert t (OnCoflictIgnore t b) m where
|
||
insert (OnCoflictIgnore values) = do
|
||
SQL.insert [qc|insert into {tn} ({n}) values({v}) on conflict ({pk}) do nothing|] bound
|
||
where
|
||
v = coerce @_ @Text $ bindListPart values
|
||
n = coerce @_ @Text $ columnListPart values
|
||
bound = toBoundList values
|
||
tn = coerce @_ @Text (tableName @t)
|
||
pk = coerce @_ @Text $ columnListPart $ primaryKey @t
|
||
|