generic-persistence-0.7.0.0: src/Database/GP/SqlGenerator.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
module Database.GP.SqlGenerator
( insertStmtFor,
insertReturningStmtFor,
updateStmtFor,
upsertStmtFor,
selectFromStmt,
countStmtFor,
deleteStmtFor,
createTableStmtFor,
dropTableStmtFor,
columnTypeFor,
WhereClauseExpr,
Field,
field,
whereClauseValues,
(&&.),
(||.),
(=.),
(>.),
(<.),
(>=.),
(<=.),
(<>.),
like,
between,
in',
isNull,
not',
sqlFun,
allEntries,
byId,
byIdColumn,
orderBy,
SortOrder (..),
limit,
limitOffset,
NonEmpty (..),
Database (..),
defaultSqliteMapping,
defaultPostgresMapping,
ColumnTypeMapping,
)
where
import Data.List (intercalate)
import Database.GP.Entity
import Database.GP.Query
-- |
-- This module defines some basic SQL statements for Record Data Types that are instances of 'Entity'.
-- The SQL statements are generated using Haskell generics to provide compile time reflection capabilities.
-- | A function that returns an SQL insert statement for an entity. Type 'a' must be an instance of Data.
-- The function will use the field names of the data type to generate the column names in the insert statement.
-- The values of the fields will be used as the values in the insert statement.
-- Output example: INSERT INTO Person (id, name, age, address) VALUES (123456, "Alice", 25, "123 Main St");
insertStmtFor :: forall a. Entity a => String
insertStmtFor =
"INSERT INTO "
++ tableName @a
++ " ("
++ intercalate ", " insertCols
++ ") VALUES ("
++ intercalate ", " (params (length insertCols))
++ ");"
where
insertCols = insertColumns @a
upsertStmtFor :: forall a. Entity a => String
upsertStmtFor =
"INSERT INTO "
++ tableName @a
++ " ("
++ intercalate ", " insertCols
++ ") VALUES ("
++ intercalate ", " (params (length insertCols))
++ ") ON CONFLICT ("
++ idColumn @a
++ ") DO UPDATE SET "
++ intercalate ", " updatePairs
++ ";"
where
insertCols = columnNamesFor @a
updatePairs = map (++ " = ?") insertCols
insertColumns :: forall a. Entity a => [String]
insertColumns =
if autoIncrement @a
then filter (/= idColumn @a) columns
else columns
where
columns = columnNamesFor @a
insertReturningStmtFor :: forall a. Entity a => String
insertReturningStmtFor =
"INSERT INTO "
++ tableName @a
++ " ("
++ intercalate ", " insertCols
++ ") VALUES ("
++ intercalate ", " (params (length insertCols))
++ ") RETURNING "
++ returnCols
++ ";"
where
insertCols = insertColumns @a
returnCols = intercalate ", " (columnNamesFor @a)
columnNamesFor :: forall a. Entity a => [String]
columnNamesFor = map snd fieldColumnPairs
where
fieldColumnPairs = fieldsToColumns @a
-- | A function that returns an SQL update statement for an entity. Type 'a' must be an instance of Entity.
updateStmtFor :: forall a. (Entity a) => String
updateStmtFor =
"UPDATE "
++ tableName @a
++ " SET "
++ intercalate ", " updatePairs
++ " WHERE "
++ idColumn @a
++ " = ?"
++ ";"
where
updatePairs = map (++ " = ?") (columnNamesFor @a)
-- | A function that returns an SQL select statement for an entity. Type 'a' must be an instance of Entity.
-- The function takes a where clause expression as parameter. This expression is used to filter the result set.
selectFromStmt :: forall a. (Entity a) => WhereClauseExpr -> String
selectFromStmt whereClauseExpr =
"SELECT "
++ intercalate ", " (columnNamesFor @a)
++ " FROM "
++ tableName @a
++ " WHERE "
++ whereClauseExprToSql @a whereClauseExpr
++ ";"
-- | A function that returns an SQL count statement for an entity. Type 'a' must be an instance of Entity.
-- The function takes a where clause expression as parameter. This expression is used to filter the result set.
countStmtFor :: forall a. (Entity a) => WhereClauseExpr -> String
countStmtFor whereClauseExpr =
"SELECT COUNT(*) FROM "
++ tableName @a
++ " WHERE "
++ whereClauseExprToSql @a whereClauseExpr
++ ";"
-- | A function that returns an SQL delete statement for an entity. Type 'a' must be an instance of Entity.
deleteStmtFor :: forall a. (Entity a) => String
deleteStmtFor =
"DELETE FROM "
++ tableName @a
++ " WHERE "
++ idColumn @a
++ " = ?;"
-- | An enumeration of the supported database types.
data Database = Postgres | SQLite
-- | A function that returns an SQL create table statement for an entity type. Type 'a' must be an instance of Entity.
createTableStmtFor :: forall a. (Entity a) => ColumnTypeMapping -> String
createTableStmtFor columnTypeMapping =
"CREATE TABLE "
++ tableName @a
++ " ("
++ intercalate ", " (map (\(f, c) -> c ++ " " ++ columnTypeFor @a columnTypeMapping f ++ optionalPK f) (fieldsToColumns @a))
++ ");"
where
optionalPK f = if isIdField @a f then " PRIMARY KEY" else ""
isIdField :: forall a. (Entity a) => String -> Bool
isIdField f = f == idField @a
-- | A function that returns the column type for a field of an entity.
-- The function takes a column type mapping function as parameter.
-- This function is used to map Haskell field types to SQL column types.
columnTypeFor :: forall a. (Entity a) => ColumnTypeMapping -> String -> String
columnTypeFor columnTypeMapping fieldName = columnTypeMapping fType
where
fType =
if isIdField @a fieldName
then "primaryKey"
else maybe "OTHER" show $ maybeFieldTypeFor @a fieldName
-- | A type alias for mapping a Haskell field type to a SQL column type.
-- this type can be used to define custom mappings for field types.
type ColumnTypeMapping = String -> String
-- | The default mapping for SQLite databases.
-- This mapping is used when no custom mapping is provided.
defaultSqliteMapping :: ColumnTypeMapping
defaultSqliteMapping = \case
"primaryKey" -> "INTEGER"
"Int" -> "INTEGER"
"[Char]" -> "TEXT"
"Double" -> "REAL"
"Float" -> "REAL"
"Bool" -> "INT"
_ -> "TEXT"
-- | The default mapping for Postgres databases.
-- This mapping is used when no custom mapping is provided.
defaultPostgresMapping :: ColumnTypeMapping
defaultPostgresMapping = \case
"primaryKey" -> "serial"
"Int" -> "numeric"
"[Char]" -> "varchar"
"Double" -> "numeric"
"Float" -> "numeric"
"Bool" -> "boolean"
_ -> "varchar"
-- | This function generates a DROP TABLE statement for an entity type.
dropTableStmtFor :: forall a. (Entity a) => String
dropTableStmtFor =
"DROP TABLE IF EXISTS "
++ tableName @a
++ ";"