packages feed

positron-0.1.0.0: library/Positron.hs

module Positron
    ( model
    , Column
    , Property(..)
    , (//)
    , Query(..)
    , Condition
    , (.==)
    , SetValue
    , (?=)
    , whose
    , Parameter(..)
    , orderBy
    , OrderBy(..)
    , onConflict
    , returning

    , module Positron.Alias
    , module Positron.Query
    , mkPositron

    , Positron
    , ConnConf
    , defaultConnConf
    , setDBHost
    , setDBPort
    , setDBName
    , setDBUser
    , setDBPassword
    , connect
    , close
    , withDatabase

    , PositronError(..)
    , textShow

    -- re-export data types
    , NonEmpty(..)
    , Int16
    , Int32
    , Int64
    , Scientific
    , Float
    , Double
    , Word16
    , Word32
    , Word64
    , ByteString
    , Text
    , UUID(..)

    ) where

import Positron.Import

-- local modules

import Positron.Alias
import Positron.Driver
import Positron.Query
import Positron.Types
import Positron.UUID
import Positron.Unsafe
import Positron.Util

mkPositron :: String -> Q [Dec]
mkPositron namespace = do
    -- record field names that cannot be captured
    connField <- newName "conn"
    connType <- [t| Connection |]
    lockField <- newName "lock"
    lockType <- [t| MVar () |]

    let
        rec fieldName fieldType =
            (fieldName, Bang NoSourceUnpackedness SourceStrict, fieldType)
        recs =
            [ rec connField connType
            , rec lockField lockType
            ]
        dataDec = DataD [] (mkName $ "Positron" <> namespace) [] Nothing
            [RecC dataName recs] []

    pairs <- runIO readPrepared

    createQueries <- fold . reverse . map buildCreateQuery <$>
        getCurrentTableMap
    instanceDec <- [d|
        instance Positron $(return $ ConT dataName) where
            pConn = $(return $ VarE connField)
            pLock = $(return $ VarE lockField)
            pPrepareds = const pairs
            pCreateQueries = const createQueries
            pMake conn = do
                lock <- newMVar ()
                return $ $(return $ ConE dataName) conn lock
        |]

    return $ dataDec : instanceDec
  where
    dataName = mkName $ "Positron" <> namespace
    buildCreateQuery (tableName, columnPairs) = let
        snakeTableName = snake (decap tableName)
        columns = map snd columnPairs
        primaryKeys = map (snake . acn) $ filter acp columns
        indexedKeys = map (snake . acn) $ filter aci columns
        foreignKeys = map formatForeignKey $ mapMaybe
            (\ c -> fmap (\ x -> (acn c, x)) (acf c)) columns
        in fold
            [ "CREATE TABLE IF NOT EXISTS "
            , snakeTableName
            , " (\n    "
            , fold $ for columns $ \AC{..} -> fold
                [ snake acn, " ", show act
                , if acUnique then " UNIQUE" else ""
                , if acnl then " NULL" else " NOT NULL"
                , ",\n    "
                ]
            , "PRIMARY KEY ("
            , intercalate ", " primaryKeys
            , ")"
            , if foreignKeys /= []
                then foldMap (",\n    " ++) foreignKeys
                else mempty
            , "\n);\n"
            , if indexedKeys /= []
                then fold $ for indexedKeys $ \columnName -> fold
                    [ "CREATE INDEX IF NOT EXISTS ix_"
                    , snakeTableName, "_", columnName
                    , " ON ", snakeTableName, " (", columnName, ");\n"
                    ]
                else mempty
            ]
    formatForeignKey (columnName, (targetTableName, targetColumnName)) = fold
        [ "FOREIGN KEY(", snake columnName, ") REFERENCES "
        , targetTableName, " (", snake targetColumnName, ")"
        ]

model :: String -> [Column] -> Q [Dec]
model tableName plainColumns = do
    columns <- mapM (analyze tableName) plainColumns
    thisModuleStr <- show <$> thisModule
    addTable thisModuleStr (tableName, [(acn, a) | a@AC{..} <- columns])
    let
        recs = for columns $ \ac@AC{..} ->
            ( mkName acFullName
            , Bang
                (if acnl then NoSourceUnpackedness else SourceUnpack)
                SourceStrict
            , columnTypeCon ac
            )

    condDecs <- fmap fold <$> forM columns $ \ AC{..} -> let
        condName = mkName $ acFullName ++ "EqParam"
      in do
        defAST <- [| Condition acn Parameter |]
        return
            [ SigD condName $ ConT ''Condition
            , ValD (VarP condName) (NormalB defAST) []
            ]

    return $
        DataD [] dataName [] Nothing [RecC dataName recs]
            [DerivClause Nothing [ConT ''Eq, ConT ''Show]]
        : condDecs
  where
    dataName = mkName tableName

analyze :: String -> Column -> Q AnalyzedColumn
analyze tableName (Column n t pk idx nl unique) = case t of
    Psmallint -> ret DBsmallint
    Pinteger -> ret DBinteger
    Pbigint -> ret DBbigint
    Pdecimal -> ret DBdecimal
    Pnumeric -> ret DBnumeric
    Preal -> ret DBreal
    Pdouble -> ret DBdouble
    Psmallserial -> ret DBsmallserial
    Pserial -> ret DBserial
    Pbigserial -> ret DBbigserial
    Pvarchar len -> ret $ DBvarchar len
    Ptext -> ret DBtext
    Puuid -> ret DBuuid
    Pforeignkey s -> do
        let
            (tn, dottedColName) = break (== '.') s
            cn = tail dottedColName
        thisModuleStr <- show <$> thisModule
        lookupColumn thisModuleStr tn cn >>= \case
            Nothing -> fail $ fold
                ["Column \"", cn, "\" of Table \"", tn, "\" not found"]
            Just AC{..} -> return $ acBase (plain act) (Just (tn, cn))
  where
    ret dt = return (acBase dt Nothing)
    acBase = AC n fullName pk idx nl unique
    plain = \case
        DBsmallserial -> DBsmallint
        DBserial -> DBinteger
        DBbigserial -> DBbigint
        x -> x
    fullName = decap tableName ++ cap n