Hello community,
here is the log from the commit of package ghc-persistent for
openSUSE:Leap:15.2 checked in at 2020-03-10 17:14:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Leap:15.2/ghc-persistent (Old)
and /work/SRC/openSUSE:Leap:15.2/.ghc-persistent.new.26092 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ghc-persistent"
Tue Mar 10 17:14:31 2020 rev:14 rq:783351 version:2.10.5.2
Changes:
--------
--- /work/SRC/openSUSE:Leap:15.2/ghc-persistent/ghc-persistent.changes
2020-02-19 18:40:34.126142998 +0100
+++
/work/SRC/openSUSE:Leap:15.2/.ghc-persistent.new.26092/ghc-persistent.changes
2020-03-10 17:14:32.973437598 +0100
@@ -1,0 +2,30 @@
+Thu Feb 27 14:18:05 UTC 2020 - [email protected]
+
+- Update persistent to version 2.10.5.2.
+ ## 2.10.5.2
+
+ * [#1041](https://github.com/yesodweb/persistent/pull/1041)
+ * Explicit foreign keys can now reference tables with custom sql name
+ * Add qualified names to the stock classes list.
+
+-------------------------------------------------------------------
+Fri Feb 7 08:04:19 UTC 2020 - [email protected]
+
+- Update persistent to version 2.10.5.1.
+ ## 2.10.5.1
+
+ * [#1024](https://github.com/yesodweb/persistent/pull/1024)
+ * Add the ability to do documentation comments in entity definition
syntax. Unfortunately, TemplateHaskell cannot add documentation comments, so
this can't be used to add Haddocks to entities.
+ * Add Haddock explainers for some of the supported entity syntax in
`Database.Persist.Quasi`
+
+-------------------------------------------------------------------
+Wed Jan 29 03:01:47 UTC 2020 - [email protected]
+
+- Update persistent to version 2.10.5.
+ ## 2.10.5
+
+ * Add the `EntityWithPrefix` type to allow users to specify a custom prefix
for raw SQL queries. [#1018](https://github.com/yesodweb/persistent/pull/1018)
+ * Added Acquire based API to `Database.Persist.Sql` for working with
+ connections/pools in monads which aren't MonadUnliftIO.
[#984](https://github.com/yesodweb/persistent/pull/984)
+
+-------------------------------------------------------------------
Old:
----
persistent-2.10.4.tar.gz
New:
----
persistent-2.10.5.2.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ ghc-persistent.spec ++++++
--- /var/tmp/diff_new_pack.749r9r/_old 2020-03-10 17:14:33.405437732 +0100
+++ /var/tmp/diff_new_pack.749r9r/_new 2020-03-10 17:14:33.409437733 +0100
@@ -1,7 +1,7 @@
#
# spec file for package ghc-persistent
#
-# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2020 SUSE LINUX GmbH, Nuernberg, Germany.
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
%global pkg_name persistent
%bcond_with tests
Name: ghc-%{pkg_name}
-Version: 2.10.4
+Version: 2.10.5.2
Release: 0
Summary: Type-safe, multi-backend data serialization
License: MIT
++++++ persistent-2.10.4.tar.gz -> persistent-2.10.5.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/persistent-2.10.4/ChangeLog.md
new/persistent-2.10.5.2/ChangeLog.md
--- old/persistent-2.10.4/ChangeLog.md 2019-10-29 18:51:02.000000000 +0100
+++ new/persistent-2.10.5.2/ChangeLog.md 2020-02-15 01:07:11.000000000
+0100
@@ -1,5 +1,23 @@
# Changelog for persistent
+## 2.10.5.2
+
+* [#1041](https://github.com/yesodweb/persistent/pull/1041)
+ * Explicit foreign keys can now reference tables with custom sql name
+ * Add qualified names to the stock classes list.
+
+## 2.10.5.1
+
+* [#1024](https://github.com/yesodweb/persistent/pull/1024)
+ * Add the ability to do documentation comments in entity definition
syntax. Unfortunately, TemplateHaskell cannot add documentation comments, so
this can't be used to add Haddocks to entities.
+ * Add Haddock explainers for some of the supported entity syntax in
`Database.Persist.Quasi`
+
+## 2.10.5
+
+* Add the `EntityWithPrefix` type to allow users to specify a custom prefix
for raw SQL queries. [#1018](https://github.com/yesodweb/persistent/pull/1018)
+* Added Acquire based API to `Database.Persist.Sql` for working with
+ connections/pools in monads which aren't MonadUnliftIO.
[#984](https://github.com/yesodweb/persistent/pull/984)
+
## 2.10.4
* Log exceptions when closing a connection fails. See point 1 in [yesod
#1635](https://github.com/yesodweb/yesod/issues/1635#issuecomment-547300856).
[#978](https://github.com/yesodweb/persistent/pull/978)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/persistent-2.10.4/Database/Persist/Quasi.hs
new/persistent-2.10.5.2/Database/Persist/Quasi.hs
--- old/persistent-2.10.4/Database/Persist/Quasi.hs 2019-10-29
08:41:44.000000000 +0100
+++ new/persistent-2.10.5.2/Database/Persist/Quasi.hs 2020-02-15
01:07:11.000000000 +0100
@@ -1,6 +1,243 @@
-{-# LANGUAGE CPP #-}
+{-# LANGUAGE BangPatterns, CPP #-}
+{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE PatternGuards #-}
+{-# LANGUAGE StandaloneDeriving, UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
+
+{-|
+This module defines the Persistent entity syntax used in the quasiquoter to
generate persistent entities.
+
+The basic structure of the syntax looks like this:
+
+> TableName
+> fieldName FieldType
+> otherField String
+> nullableField Int Maybe
+
+You start an entity definition with the table name, in this case, @TableName@.
It's followed by a list of fields on the entity, which have the basic form
@fieldName FieldType@. You can indicate that a field is nullable with 'Maybe'
at the end of the type.
+
+@persistent@ automatically generates an ID column for you, if you don't
specify one, so the above table definition corresponds to the following SQL:
+
+> CREATE TABLE table_name (
+> id SERIAL PRIMARY KEY,
+> field_name field_type NOT NULL,
+> other_field varchar NOT NULL,
+> nullable_field int NULL
+> );
+
+Note that the exact SQL that is generated can be customized using the
'PersistSettings' that are passed to the 'parse' function.
+
+It generates a Haskell datatype with the following form:
+
+@
+data TableName = TableName
+ { tableNameFieldName :: FieldType
+ , tableNameOtherField :: String
+ , tableNameNullableField :: Maybe Int
+ }
+@
+
+As with the SQL generated, the specifics of this are customizable.
+See the @persistent-template@ package for details.
+
+= Deriving
+
+You can add a deriving clause to a table, and the generated Haskell type will
have a deriving clause with that.
+Unlike normal Haskell syntax, you don't need parentheses or commas to separate
the classes, and you can even have multiple deriving clauses.
+
+> User
+> name String
+> age Int
+> deriving Eq Show
+> deriving Ord
+
+= Unique Keys
+
+You can define a uniqueness key on a table with the following format:
+
+> User
+> name String
+> age Int
+>
+> UniqueUserName name
+
+This will put a unique index on the @user@ table and the @name@ field.
+
+= Setting defaults
+
+You can use a @default=${sql expression}@ clause to set a default for a field.
+The thing following the `=` is interpreted as SQL that will be put directly
into the table definition.
+
+@
+User
+ name Text
+ admin Bool default=false
+@
+
+This creates a SQL definition like this:
+
+> CREATE TABLE user (
+> id SERIAL PRIMARY KEY,
+> name VARCHAR NOT NULL,
+> admin BOOL DEFAULT=false
+> );
+
+A restriction here is that you still need to provide a value when performing
an `insert`, because the generated Haskell type has the form:
+
+@
+data User = User
+ { userName :: Text
+ , userAdmin :: Bool
+ }
+@
+
+You can work around this by using a 'Maybe Bool' and supplying 'Nothing' by
default.
+
+= Custom ID column
+
+If you don't want to use the default ID column type of 'Int64', you can set a
custom type with an @Id@ field.
+This @User@ has a @Text@ ID.
+
+> User
+> Id Text
+> name Text
+> age Int
+
+If you do this, it's a good idea to set a default for the ID.
+Otherwise, you will need to use 'insertKey' instead of 'insert' when
performing inserts.
+
+@
+'insertKey' (UserKey "Hello world!") (User "Bob" 32)
+@
+
+If you attempt to do @'insert' (User "Bob" 32)@, then you will receive a
runtime error because the SQL database doesn't know how to make an ID for you
anymore.
+So instead just use a default expression, like this:
+
+@
+User
+ Id Text default=generate_user_id()
+ name Text
+ age Int
+@
+
+= Custom Primary Keys
+
+Sometimes you don't want to have an ID column, and you want a different sort
of primary key.
+This is a table that stores unique email addresses, and the email is the
primary key.
+We store the first and second part (eg @first\@second@) separately.
+
+@
+Email
+ firstPart Text
+ secondPart Text
+
+ Primary firstPart secondPart
+@
+
+This creates a table with the following form:
+
+@
+CREATE TABLE email (
+ first_part varchar,
+ second_part varchar,
+
+ PRIMARY KEY (first_part, second_part)
+@
+
+You can specify 1 or more columns in the primary key.
+
+= Overriding SQL
+
+You can use a @sql=custom@ annotation to provide some customization on the
entity and field.
+For example, you might prefer to name a table differently than what
@persistent@ will do by default.
+You may also prefer to name a field differently.
+
+@
+User sql=big_user_table
+ fullName String sql=name
+ age Int
+@
+
+This will alter the generated SQL to be:
+
+@
+CREATE TABEL big_user_table (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR,
+ age INT
+);
+@
+
+= Attributes
+
+The QuasiQuoter allows you to provide arbitrary attributes to an entity or
field.
+This can be used to extend the code in ways that the library hasn't
anticipated.
+If you use this feature, we'd definitely appreciate hearing about it and
potentially supporting your use case directly!
+
+@
+User !funny
+ field String !sad
+ good Dog !sogood
+@
+
+We can see the attributes using the 'entityAttrs' field and the 'fieldAttrs'
field.
+
+@
+userAttrs = do
+ let userDefinition = 'entityDef' ('Proxy' :: 'Proxy' User)
+ let userAttributes = 'entityAttrs' userDefinition
+ let fieldAttributes = 'map' 'fieldAttrs' ('entityFields' userDefinition)
+ print userAttributes
+-- ["funny"]
+ print fieldAttributes
+-- [["sad"],["sogood"]]
+@
+
+= Documentation Comments
+
+The quasiquoter supports ordinary comments with @--@ and @#@.
+Since @persistent-2.10.5.1@, it also supports documentation comments.
+The grammar for documentation comments is similar to Haskell's Haddock syntax,
with a few restrictions:
+
+1. Only the @-- | @ form is allowed.
+2. You must put a space before and after the @|@ pipe character.
+3. The comment must be indented at the same level as the entity or field it
documents.
+
+An example of the field documentation is:
+
+@
+-- | I am a doc comment for a User. Users are important
+-- | to the application, and should be treasured.
+User
+ -- | Users have names. Call them by names.
+ name String
+ -- | A user can be old, or young, and we care about
+ -- | this for some reason.
+ age Int
+@
+
+The documentation is present on the `entityComments` field on the `EntityDef`
for the entity:
+
+@
+>>> let userDefinition = entityDef (Proxy :: Proxy User)
+>>> entityComments userDefinition
+"I am a doc comment for a User. Users are important\nto the application, and
should be treasured.\n"
+@
+
+Likewise, the field documentation is present in the `fieldComments` field on
the `FieldDef` present in the `EntityDef`:
+
+@
+>>> let userFields = entityFields userDefinition
+>>> let comments = map fieldComments userFields
+>>> mapM_ putStrLn comments
+"Users have names. Call them by names."
+"A user can be old, or young, and we care about\nthis for some reason."
+@
+
+Unfortunately, we can't use this to create Haddocks for you, because
<https://gitlab.haskell.org/ghc/ghc/issues/5467 Template Haskell does not
support Haddock yet>.
+`persistent` backends *can* use this to generate SQL @COMMENT@s, which are
useful for a database perspective, and you can use the
<https://hackage.haskell.org/package/persistent-documentation
@persistent-documentation@> library to render a Markdown document of the entity
definitions.
+
+-}
module Database.Persist.Quasi
( parse
, PersistSettings (..)
@@ -9,19 +246,28 @@
, nullable
#if TEST
, Token (..)
+ , Line' (..)
+ , preparse
, tokenize
, parseFieldType
+ , empty
+ , removeSpaces
+ , associateLines
+ , skipEmpty
+ , LinesWithComments(..)
#endif
) where
import Prelude hiding (lines)
+import qualified Data.List.NonEmpty as NEL
+import Data.List.NonEmpty (NonEmpty(..))
import Control.Arrow ((&&&))
import Control.Monad (msum, mplus)
import Data.Char
import Data.List (find, foldl')
import qualified Data.Map as M
-import Data.Maybe (mapMaybe, fromMaybe, maybeToList)
+import Data.Maybe (mapMaybe, fromMaybe, maybeToList, listToMaybe)
import Data.Monoid (mappend)
import Data.Text (Text)
import qualified Data.Text as T
@@ -109,21 +355,26 @@
-- | Parses a quasi-quoted syntax into a list of entity definitions.
parse :: PersistSettings -> Text -> [EntityDef]
-parse ps = parseLines ps
- . removeSpaces
- . filter (not . empty)
- . map tokenize
- . T.lines
+parse ps = parseLines ps . preparse
+
+preparse :: Text -> [Line]
+preparse =
+ removeSpaces
+ . filter (not . empty)
+ . map tokenize
+ . T.lines
-- | A token used by the parser.
data Token = Spaces !Int -- ^ @Spaces n@ are @n@ consecutive spaces.
| Token Text -- ^ @Token tok@ is token @tok@ already unquoted.
+ | DocComment Text -- ^ @DocComment@ is a documentation comment,
unmodified.
deriving (Show, Eq)
-- | Tokenize a string.
tokenize :: Text -> [Token]
tokenize t
| T.null t = []
+ | "-- | " `T.isPrefixOf` t = [DocComment t]
| "--" `T.isPrefixOf` t = [] -- Comment until the end of the line.
| "#" `T.isPrefixOf` t = [] -- Also comment to the end of the line, needed
for a CPP bug (#110)
| T.head t == '"' = quotes (T.tail t) id
@@ -181,9 +432,22 @@
-- | A line. We don't care about spaces in the middle of the
-- line. Also, we don't care about the ammount of indentation.
-data Line = Line { lineIndent :: Int
- , tokens :: [Text]
- }
+data Line' f
+ = Line
+ { lineIndent :: Int
+ , tokens :: f Text
+ }
+
+deriving instance Show (f Text) => Show (Line' f)
+deriving instance Eq (f Text) => Eq (Line' f)
+
+mapLine :: (forall x. f x -> g x) -> Line' f -> Line' g
+mapLine k (Line i t) = Line i (k t)
+
+traverseLine :: Functor t => (forall x. f x -> t (g x)) -> Line' f -> t (Line'
g)
+traverseLine k (Line i xs) = Line i <$> k xs
+
+type Line = Line' []
-- | Remove leading spaces and remove spaces in the middle of the
-- tokens.
@@ -197,6 +461,7 @@
toLine' i = Line i . mapMaybe fromToken
fromToken (Token t) = Just t
+ fromToken (DocComment t) = Just t
fromToken Spaces{} = Nothing
-- | Divide lines into blocks and make entity definitions.
@@ -204,11 +469,87 @@
parseLines ps lines =
fixForeignKeysAll $ toEnts lines
where
- toEnts (Line indent (name:entattribs) : rest) =
- let (x, y) = span ((> indent) . lineIndent) rest
- in mkEntityDef ps name entattribs x : toEnts y
- toEnts (Line _ []:rest) = toEnts rest
- toEnts [] = []
+ toEnts :: [Line] -> [UnboundEntityDef]
+ toEnts =
+ map mk
+ . associateLines
+ . skipEmpty
+ mk :: LinesWithComments -> UnboundEntityDef
+ mk lwc =
+ let Line _ (name :| entAttribs) :| rest = lwcLines lwc
+ in setComments (lwcComments lwc) $ mkEntityDef ps name entAttribs
(map (mapLine NEL.toList) rest)
+
+isComment :: Text -> Maybe Text
+isComment xs =
+ T.stripPrefix "-- | " xs
+
+data LinesWithComments = LinesWithComments
+ { lwcLines :: NonEmpty (Line' NonEmpty)
+ , lwcComments :: [Text]
+ } deriving (Eq, Show)
+
+-- TODO: drop this and use <> when 8.2 isn't supported anymore so the
+-- monoid/semigroup nonsense isn't annoying
+appendLwc :: LinesWithComments -> LinesWithComments -> LinesWithComments
+appendLwc a b =
+ LinesWithComments (foldr NEL.cons (lwcLines b) (lwcLines a)) (lwcComments
a `mappend` lwcComments b)
+
+newLine :: Line' NonEmpty -> LinesWithComments
+newLine l = LinesWithComments (pure l) []
+
+firstLine :: LinesWithComments -> Line' NonEmpty
+firstLine = NEL.head . lwcLines
+
+consLine :: Line' NonEmpty -> LinesWithComments -> LinesWithComments
+consLine l lwc = lwc { lwcLines = NEL.cons l (lwcLines lwc) }
+
+consComment :: Text -> LinesWithComments -> LinesWithComments
+consComment l lwc = lwc { lwcComments = l : lwcComments lwc }
+
+associateLines :: [Line' NonEmpty] -> [LinesWithComments]
+associateLines lines =
+ foldr combine [] $
+ foldr toLinesWithComments [] lines
+ where
+ toLinesWithComments line linesWithComments =
+ case linesWithComments of
+ [] ->
+ [newLine line]
+ (lwc : lwcs) ->
+ case isComment (NEL.head (tokens line)) of
+ Just comment
+ | lineIndent line == lowestIndent ->
+ consComment comment lwc : lwcs
+ _ ->
+ if lineIndent line <= lineIndent (firstLine lwc)
+ then
+ consLine line lwc : lwcs
+ else
+ newLine line : lwc : lwcs
+
+ lowestIndent = minimum . fmap lineIndent $ lines
+ combine :: LinesWithComments -> [LinesWithComments] -> [LinesWithComments]
+ combine lwc [] =
+ [lwc]
+ combine lwc (lwc' : lwcs) =
+ let minIndent = minimumIndentOf lwc
+ otherIndent = minimumIndentOf lwc'
+ in
+ if minIndent < otherIndent then
+ appendLwc lwc lwc' : lwcs
+ else
+ lwc : lwc' : lwcs
+
+
+ minimumIndentOf = minimum . fmap lineIndent . lwcLines
+
+skipEmpty :: [Line' []] -> [Line' NonEmpty]
+skipEmpty = mapMaybe (traverseLine NEL.nonEmpty)
+
+setComments :: [Text] -> UnboundEntityDef -> UnboundEntityDef
+setComments [] = id
+setComments comments =
+ overUnboundEntityDef (\ed -> ed { entityComments = Just (T.unlines
comments) })
fixForeignKeysAll :: [UnboundEntityDef] -> [EntityDef]
fixForeignKeysAll unEnts = map fixForeignKeys unEnts
@@ -223,24 +564,41 @@
-- check the count and the sqltypes match and update the foreignFields
with the names of the primary columns
fixForeignKey :: EntityDef -> UnboundForeignDef -> ForeignDef
fixForeignKey ent (UnboundForeignDef foreignFieldTexts fdef) =
- case M.lookup (foreignRefTableHaskell fdef) entLookup of
- Just pent -> case entityPrimary pent of
- Just pdef ->
- if length foreignFieldTexts /= length (compositeFields pdef)
- then lengthError pdef
- else let fds_ffs = zipWith (toForeignFields pent)
- foreignFieldTexts
- (compositeFields pdef)
- in fdef { foreignFields = map snd fds_ffs
- , foreignNullable = setNull $ map fst fds_ffs
- }
- Nothing ->
- error $ "no explicit primary key fdef="++show fdef++ "
ent="++show ent
- Nothing ->
- error $ "could not find table " ++ show (foreignRefTableHaskell
fdef)
- ++ " fdef=" ++ show fdef ++ " allnames="
- ++ show (map (unHaskellName . entityHaskell . unboundEntityDef)
unEnts)
- ++ "\n\nents=" ++ show ents
+ let pentError =
+ error $ "could not find table " ++ show
(foreignRefTableHaskell fdef)
+ ++ " fdef=" ++ show fdef ++ " allnames="
+ ++ show (map (unHaskellName . entityHaskell .
unboundEntityDef) unEnts)
+ ++ "\n\nents=" ++ show ents
+ pent =
+ fromMaybe pentError $ M.lookup (foreignRefTableHaskell fdef)
entLookup
+ in
+ case entityPrimary pent of
+ Just pdef ->
+ if length foreignFieldTexts /= length (compositeFields
pdef)
+ then
+ lengthError pdef
+ else
+ let
+ fds_ffs =
+ zipWith (toForeignFields pent)
+ foreignFieldTexts
+ (compositeFields pdef)
+ dbname =
+ unDBName (entityDB pent)
+ oldDbName =
+ unDBName (foreignRefTableDBName fdef)
+ in fdef
+ { foreignFields = map snd fds_ffs
+ , foreignNullable = setNull $ map fst fds_ffs
+ , foreignRefTableDBName =
+ DBName dbname
+ , foreignConstraintNameDBName =
+ DBName
+ . T.replace oldDbName dbname . unDBName
+ $ foreignConstraintNameDBName fdef
+ }
+ Nothing ->
+ error $ "no explicit primary key fdef="++show fdef++ "
ent="++show ent
where
setNull :: [FieldDef] -> Bool
setNull [] = error "setNull: impossible!"
@@ -282,9 +640,14 @@
, unboundEntityDef :: EntityDef
}
+overUnboundEntityDef
+ :: (EntityDef -> EntityDef) -> UnboundEntityDef -> UnboundEntityDef
+overUnboundEntityDef f ubed =
+ ubed { unboundEntityDef = f (unboundEntityDef ubed) }
lookupKeyVal :: Text -> [Text] -> Maybe Text
lookupKeyVal key = lookupPrefix $ key `mappend` "="
+
lookupPrefix :: Text -> [Text] -> Maybe Text
lookupPrefix prefix = msum . map (T.stripPrefix prefix)
@@ -297,20 +660,21 @@
mkEntityDef ps name entattribs lines =
UnboundEntityDef foreigns $
EntityDef
- entName
- (DBName $ getDbName ps name' entattribs)
+ { entityHaskell = entName
+ , entityDB = DBName $ getDbName ps name' entattribs
-- idField is the user-specified Id
-- otherwise useAutoIdField
-- but, adjust it if the user specified a Primary
- (setComposite primaryComposite $ fromMaybe autoIdField idField)
- entattribs
- cols
- uniqs
- []
- derives
- extras
- isSum
- comments
+ , entityId = (setComposite primaryComposite $ fromMaybe autoIdField
idField)
+ , entityAttrs = entattribs
+ , entityFields = cols
+ , entityUniques = uniqs
+ , entityForeigns = []
+ , entityDerives = derives
+ , entityExtra = extras
+ , entitySum = isSum
+ , entityComments = comments
+ }
where
comments = Nothing
entName = HaskellName name'
@@ -332,7 +696,18 @@
derives = concat $ mapMaybe takeDerives attribs
cols :: [FieldDef]
- cols = mapMaybe (takeColsEx ps) attribs
+ cols = reverse . fst . foldr k ([], []) $ reverse attribs
+ k x (!acc, !comments) =
+ case isComment =<< listToMaybe x of
+ Just comment ->
+ (acc, comment : comments)
+ Nothing ->
+ ( maybe id (:) (setFieldComments comments <$> takeColsEx ps x)
acc
+ , []
+ )
+ setFieldComments [] x = x
+ setFieldComments xs fld =
+ fld { fieldComments = Just (T.unlines xs) }
autoIdField = mkAutoIdField ps entName (DBName `fmap` idName) idSqlType
idSqlType = maybe SqlInt64 (const $ SqlOther "Primary Key")
primaryComposite
@@ -382,9 +757,15 @@
in (ts:x, y)
takeColsEx :: PersistSettings -> [Text] -> Maybe FieldDef
-takeColsEx = takeCols (\ft perr -> error $ "Invalid field type " ++ show ft ++
" " ++ perr)
-
-takeCols :: (Text -> String -> Maybe FieldDef) -> PersistSettings -> [Text] ->
Maybe FieldDef
+takeColsEx =
+ takeCols
+ (\ft perr -> error $ "Invalid field type " ++ show ft ++ " " ++ perr)
+
+takeCols
+ :: (Text -> String -> Maybe FieldDef)
+ -> PersistSettings
+ -> [Text]
+ -> Maybe FieldDef
takeCols _ _ ("deriving":_) = Nothing
takeCols onErr ps (n':typ:rest)
| not (T.null n) && isLower (T.head n) =
@@ -530,13 +911,21 @@
takeForeign ps tableName _defs (refTableName:n:rest)
| not (T.null n) && isLower (T.head n)
= UnboundForeignDef fields $ ForeignDef
- (HaskellName refTableName)
- (DBName $ psToDBName ps refTableName)
- (HaskellName n)
- (DBName $ psToDBName ps (tableName `T.append` n))
- []
- attrs
- False
+ { foreignRefTableHaskell =
+ HaskellName refTableName
+ , foreignRefTableDBName =
+ DBName $ psToDBName ps refTableName
+ , foreignConstraintNameHaskell =
+ HaskellName n
+ , foreignConstraintNameDBName =
+ DBName $ psToDBName ps (tableName `T.append` n)
+ , foreignFields =
+ []
+ , foreignAttrs =
+ attrs
+ , foreignNullable =
+ False
+ }
where
(fields,attrs) = break ("!" `T.isPrefixOf`) rest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/persistent-2.10.4/Database/Persist/Sql/Class.hs
new/persistent-2.10.5.2/Database/Persist/Sql/Class.hs
--- old/persistent-2.10.4/Database/Persist/Sql/Class.hs 2019-10-29
08:41:44.000000000 +0100
+++ new/persistent-2.10.5.2/Database/Persist/Sql/Class.hs 2020-02-12
01:12:05.000000000 +0100
@@ -4,11 +4,16 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE DataKinds #-}
+
module Database.Persist.Sql.Class
( RawSql (..)
, PersistFieldSql (..)
+ , EntityWithPrefix(..)
+ , unPrefix
) where
+import GHC.TypeLits (Symbol, KnownSymbol, symbolVal)
import Data.Bits (bitSizeMaybe)
import Data.ByteString (ByteString)
import Data.Fixed
@@ -17,7 +22,7 @@
import qualified Data.Map as M
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
-import Data.Proxy (Proxy)
+import Data.Proxy (Proxy(..))
import qualified Data.Set as S
import Data.Text (Text, intercalate, pack)
import qualified Data.Text as T
@@ -82,6 +87,88 @@
<*> fromPersistValues rowVal
where
nKeyFields = length $ entityKeyFields entDef
+ entDef = entityDef (Nothing :: Maybe record)
+
+-- | This newtype wrapper is useful when selecting an entity out of the
+-- database and you want to provide a prefix to the table being selected.
+--
+-- Consider this raw SQL query:
+--
+-- > SELECT ??
+-- > FROM my_long_table_name AS mltn
+-- > INNER JOIN other_table AS ot
+-- > ON mltn.some_col = ot.other_col
+-- > WHERE ...
+--
+-- We don't want to refer to @my_long_table_name@ every time, so we create
+-- an alias. If we want to select it, we have to tell the raw SQL
+-- quasi-quoter that we expect the entity to be prefixed with some other
+-- name.
+--
+-- We can give the above query a type with this, like:
+--
+-- @
+-- getStuff :: 'SqlPersistM' ['EntityWithPrefix' \"mltn\" MyLongTableName]
+-- getStuff = rawSql queryText []
+-- @
+--
+-- The 'EntityWithPrefix' bit is a boilerplate newtype wrapper, so you can
+-- remove it with 'unPrefix', like this:
+--
+-- @
+-- getStuff :: 'SqlPersistM' ['Entity' MyLongTableName]
+-- getStuff = 'unPrefix' @\"mltn\" '<$>' 'rawSql' queryText []
+-- @
+--
+-- The @ symbol is a "type application" and requires the @TypeApplications@
+-- language extension.
+--
+-- @since 2.10.5
+newtype EntityWithPrefix (prefix :: Symbol) record
+ = EntityWithPrefix { unEntityWithPrefix :: Entity record }
+
+-- | A helper function to tell GHC what the 'EntityWithPrefix' prefix
+-- should be. This allows you to use a type application to specify the
+-- prefix, instead of specifying the etype on the result.
+--
+-- As an example, here's code that uses this:
+--
+-- @
+-- myQuery :: 'SqlPersistM' ['Entity' Person]
+-- myQuery = map (unPrefix @\"p\") <$> rawSql query []
+-- where
+-- query = "SELECT ?? FROM person AS p"
+-- @
+--
+-- @since 2.10.5
+unPrefix :: forall prefix record. EntityWithPrefix prefix record -> Entity
record
+unPrefix = unEntityWithPrefix
+
+instance
+ ( PersistEntity record
+ , KnownSymbol prefix
+ , PersistEntityBackend record ~ backend
+ , IsPersistBackend backend
+ )
+ => RawSql (EntityWithPrefix prefix record) where
+ rawSqlCols escape _ent = (length sqlFields, [intercalate ", " sqlFields])
+ where
+ sqlFields = map (((name <> ".") <>) . escape)
+ $ map fieldDB
+ -- Hacky for a composite key because
+ -- it selects the same field multiple times
+ $ entityKeyFields entDef ++ entityFields entDef
+ name = pack $ symbolVal (Proxy :: Proxy prefix)
+ entDef = entityDef (Nothing :: Maybe record)
+ rawSqlColCountReason a =
+ case fst (rawSqlCols (error "RawSql") a) of
+ 1 -> "one column for an 'Entity' data type without fields"
+ n -> show n ++ " columns for an 'Entity' data type"
+ rawSqlProcessRow row = case splitAt nKeyFields row of
+ (rowKey, rowVal) -> fmap EntityWithPrefix $ Entity <$> keyFromValues
rowKey
+ <*> fromPersistValues rowVal
+ where
+ nKeyFields = length $ entityKeyFields entDef
entDef = entityDef (Nothing :: Maybe record)
-- | @since 1.0.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/persistent-2.10.4/Database/Persist/Sql/Run.hs
new/persistent-2.10.5.2/Database/Persist/Sql/Run.hs
--- old/persistent-2.10.4/Database/Persist/Sql/Run.hs 2019-10-29
18:51:02.000000000 +0100
+++ new/persistent-2.10.5.2/Database/Persist/Sql/Run.hs 2020-01-28
17:35:59.000000000 +0100
@@ -6,9 +6,13 @@
import Control.Monad.IO.Unlift
import qualified UnliftIO.Exception as UE
import Control.Monad.Logger.CallStack
+import Control.Monad.Reader (MonadReader)
+import qualified Control.Monad.Reader as MonadReader
import Control.Monad.Trans.Reader hiding (local)
import Control.Monad.Trans.Resource
+import Data.Acquire (Acquire, ReleaseType(..), mkAcquireType, with)
import Data.IORef (readIORef)
+import Data.Pool (Pool, LocalPool)
import Data.Pool as P
import qualified Data.Map as Map
import qualified Data.Text as T
@@ -19,6 +23,60 @@
import Database.Persist.Sql.Types.Internal (IsolationLevel)
import Database.Persist.Sql.Raw
+-- | The returned 'Acquire' gets a connection from the pool, but does __NOT__
+-- start a new transaction. Used to implement 'acquireSqlConnFromPool' and
+-- 'acquireSqlConnFromPoolWithIsolation', this is useful for performing actions
+-- on a connection that cannot be done within a transaction, such as VACUUM in
+-- Sqlite.
+--
+-- @since 2.10.5
+unsafeAcquireSqlConnFromPool
+ :: forall backend m
+ . (MonadReader (Pool backend) m, BackendCompatible SqlBackend backend)
+ => m (Acquire backend)
+unsafeAcquireSqlConnFromPool = do
+ pool <- MonadReader.ask
+
+ let freeConn :: (backend, LocalPool backend) -> ReleaseType -> IO ()
+ freeConn (res, localPool) relType = case relType of
+ ReleaseException -> P.destroyResource pool localPool res
+ _ -> P.putResource localPool res
+
+ return $ fst <$> mkAcquireType (P.takeResource pool) freeConn
+
+
+-- | The returned 'Acquire' gets a connection from the pool, starts a new
+-- transaction and gives access to the prepared connection.
+--
+-- When the acquired connection is released the transaction is committed and
+-- the connection returned to the pool.
+--
+-- Upon an exception the transaction is rolled back and the connection
+-- destroyed.
+--
+-- This is equivalent to 'runSqlPool' but does not incur the 'MonadUnliftIO'
+-- constraint, meaning it can be used within, for example, a 'Conduit'
+-- pipeline.
+--
+-- @since 2.10.5
+acquireSqlConnFromPool
+ :: (MonadReader (Pool backend) m, BackendCompatible SqlBackend backend)
+ => m (Acquire backend)
+acquireSqlConnFromPool = do
+ connFromPool <- unsafeAcquireSqlConnFromPool
+ return $ connFromPool >>= acquireSqlConn
+
+-- | Like 'acquireSqlConnFromPool', but lets you specify an explicit isolation
+-- level.
+--
+-- @since 2.10.5
+acquireSqlConnFromPoolWithIsolation
+ :: (MonadReader (Pool backend) m, BackendCompatible SqlBackend backend)
+ => IsolationLevel -> m (Acquire backend)
+acquireSqlConnFromPoolWithIsolation isolation = do
+ connFromPool <- unsafeAcquireSqlConnFromPool
+ return $ connFromPool >>= acquireSqlConnWithIsolation isolation
+
-- | Get a connection from the pool, run the given action, and then return the
-- connection to the pool.
--
@@ -28,7 +86,7 @@
runSqlPool
:: (MonadUnliftIO m, BackendCompatible SqlBackend backend)
=> ReaderT backend m a -> Pool backend -> m a
-runSqlPool r pconn = withRunInIO $ \run -> withResource pconn $ run .
runSqlConn r
+runSqlPool r pconn = with (acquireSqlConnFromPool pconn) $ runReaderT r
-- | Like 'runSqlPool', but supports specifying an isolation level.
--
@@ -36,7 +94,8 @@
runSqlPoolWithIsolation
:: (MonadUnliftIO m, BackendCompatible SqlBackend backend)
=> ReaderT backend m a -> Pool backend -> IsolationLevel -> m a
-runSqlPoolWithIsolation r pconn i = withRunInIO $ \run -> withResource pconn $
run . (\conn -> runSqlConnWithIsolation r conn i)
+runSqlPoolWithIsolation r pconn i =
+ with (acquireSqlConnFromPoolWithIsolation i pconn) $ runReaderT r
-- | Like 'withResource', but times out the operation if resource
-- allocation does not complete within the given timeout period.
@@ -60,30 +119,62 @@
return ret
{-# INLINABLE withResourceTimeout #-}
+rawAcquireSqlConn
+ :: forall backend m
+ . (MonadReader backend m, BackendCompatible SqlBackend backend)
+ => Maybe IsolationLevel -> m (Acquire backend)
+rawAcquireSqlConn isolation = do
+ conn <- MonadReader.ask
+ let rawConn :: SqlBackend
+ rawConn = projectBackend conn
+
+ getter :: T.Text -> IO Statement
+ getter = getStmtConn rawConn
+
+ beginTransaction :: IO backend
+ beginTransaction = conn <$ connBegin rawConn getter isolation
+
+ finishTransaction :: backend -> ReleaseType -> IO ()
+ finishTransaction _ relType = case relType of
+ ReleaseException -> connRollback rawConn getter
+ _ -> connCommit rawConn getter
+
+ return $ mkAcquireType beginTransaction finishTransaction
+
+-- | Starts a new transaction on the connection. When the acquired connection
+-- is released the transaction is committed and the connection returned to the
+-- pool.
+--
+-- Upon an exception the transaction is rolled back and the connection
+-- destroyed.
+--
+-- This is equivalent to 'runSqlConn but does not incur the 'MonadUnliftIO'
+-- constraint, meaning it can be used within, for example, a 'Conduit'
+-- pipeline.
+--
+-- @since 2.10.5
+acquireSqlConn
+ :: (MonadReader backend m, BackendCompatible SqlBackend backend)
+ => m (Acquire backend)
+acquireSqlConn = rawAcquireSqlConn Nothing
+
+-- | Like 'acquireSqlConn', but lets you specify an explicit isolation level.
+--
+-- @since 2.10.5
+acquireSqlConnWithIsolation
+ :: (MonadReader backend m, BackendCompatible SqlBackend backend)
+ => IsolationLevel -> m (Acquire backend)
+acquireSqlConnWithIsolation = rawAcquireSqlConn . Just
+
runSqlConn :: (MonadUnliftIO m, BackendCompatible SqlBackend backend) =>
ReaderT backend m a -> backend -> m a
-runSqlConn r conn = withRunInIO $ \runInIO -> mask $ \restore -> do
- let conn' = projectBackend conn
- getter = getStmtConn conn'
- restore $ connBegin conn' getter Nothing
- x <- onException
- (restore $ runInIO $ runReaderT r conn)
- (restore $ connRollback conn' getter)
- restore $ connCommit conn' getter
- return x
+runSqlConn r conn = with (acquireSqlConn conn) $ runReaderT r
-- | Like 'runSqlConn', but supports specifying an isolation level.
--
-- @since 2.9.0
runSqlConnWithIsolation :: (MonadUnliftIO m, BackendCompatible SqlBackend
backend) => ReaderT backend m a -> backend -> IsolationLevel -> m a
-runSqlConnWithIsolation r conn isolation = withRunInIO $ \runInIO -> mask $
\restore -> do
- let conn' = projectBackend conn
- getter = getStmtConn conn'
- restore $ connBegin conn' getter $ Just isolation
- x <- onException
- (restore $ runInIO $ runReaderT r conn)
- (restore $ connRollback conn' getter)
- restore $ connCommit conn' getter
- return x
+runSqlConnWithIsolation r conn isolation =
+ with (acquireSqlConnWithIsolation isolation conn) $ runReaderT r
runSqlPersistM
:: (BackendCompatible SqlBackend backend)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/persistent-2.10.4/Database/Persist/Sql.hs
new/persistent-2.10.5.2/Database/Persist/Sql.hs
--- old/persistent-2.10.4/Database/Persist/Sql.hs 2019-10-29
08:41:44.000000000 +0100
+++ new/persistent-2.10.5.2/Database/Persist/Sql.hs 2020-01-28
17:35:59.000000000 +0100
@@ -30,7 +30,7 @@
import Database.Persist.Sql.Types
import Database.Persist.Sql.Types.Internal (IsolationLevel (..))
import Database.Persist.Sql.Class
-import Database.Persist.Sql.Run hiding (withResourceTimeout)
+import Database.Persist.Sql.Run hiding (withResourceTimeout, rawAcquireSqlConn)
import Database.Persist.Sql.Raw
import Database.Persist.Sql.Migration
import Database.Persist.Sql.Internal
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/persistent-2.10.4/persistent.cabal
new/persistent-2.10.5.2/persistent.cabal
--- old/persistent-2.10.4/persistent.cabal 2019-10-29 18:51:02.000000000
+0100
+++ new/persistent-2.10.5.2/persistent.cabal 2020-02-15 01:07:11.000000000
+0100
@@ -1,5 +1,5 @@
name: persistent
-version: 2.10.4
+version: 2.10.5.2
license: MIT
license-file: LICENSE
author: Michael Snoyman <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/persistent-2.10.4/test/main.hs
new/persistent-2.10.5.2/test/main.hs
--- old/persistent-2.10.4/test/main.hs 2019-10-29 08:41:44.000000000 +0100
+++ new/persistent-2.10.5.2/test/main.hs 2020-02-15 01:07:11.000000000
+0100
@@ -1,4 +1,10 @@
+{-# language RecordWildCards #-}
+
import Test.Hspec
+import qualified Data.Text as T
+import Data.List.NonEmpty (NonEmpty(..))
+import qualified Data.List.NonEmpty as NEL
+import qualified Data.Map as Map
import Database.Persist.Quasi
import Database.Persist.Types
@@ -74,6 +80,21 @@
, Spaces 1
, Token "baz=bin\""
]
+ describe "comments" $ do
+ it "recognizes one line" $ do
+ tokenize "-- | this is a comment" `shouldBe`
+ [ DocComment "-- | this is a comment"
+ ]
+ it "map tokenize" $ do
+ map tokenize ["Foo", "-- | Hello"]
+ `shouldBe`
+ [ [Token "Foo"]
+ , [DocComment "-- | Hello"]
+ ]
+ it "works if comment is indented" $ do
+ tokenize " -- | comment" `shouldBe`
+ [ Spaces 2, DocComment "-- | comment"
+ ]
describe "parseFieldType" $ do
it "simple types" $
parseFieldType "FooBar" `shouldBe` Right (FTTypeCon Nothing
"FooBar")
@@ -100,3 +121,382 @@
baz = FTTypeCon Nothing "Baz"
parseFieldType "Foo [Bar] Baz" `shouldBe` Right (
foo `FTApp` bars `FTApp` baz)
+
+ describe "preparse" $ do
+ it "recognizes entity" $ do
+ preparse "Person\n name String\n age Int" `shouldBe`
+ [ Line { lineIndent = 0, tokens = ["Person"] }
+ , Line { lineIndent = 2, tokens = ["name", "String"] }
+ , Line { lineIndent = 2, tokens = ["age", "Int"] }
+ ]
+ describe "recognizes comments" $ do
+ let text = "Foo\n x X\n-- | Hello\nBar\n name String"
+ linesText = T.lines text
+ it "T.lines" $ do
+ linesText
+ `shouldBe`
+ [ "Foo"
+ , " x X"
+ , "-- | Hello"
+ , "Bar"
+ , " name String"
+ ]
+ let tokens = map tokenize linesText
+ it "map tokenize" $ do
+ tokens `shouldBe`
+ [ [ Token "Foo" ]
+ , [ Spaces 2, Token "x", Spaces 1, Token "X"]
+ , [ DocComment "-- | Hello" ]
+ , [ Token "Bar" ]
+ , [ Spaces 1, Token "name", Spaces 1, Token "String" ]
+ ]
+ let filtered = filter (not . empty) tokens
+ it "filter (not . empty)" $ do
+ filtered `shouldBe`
+ [ [ Token "Foo" ]
+ , [ Spaces 2, Token "x", Spaces 1, Token "X"]
+ , [ DocComment "-- | Hello" ]
+ , [ Token "Bar" ]
+ , [ Spaces 1, Token "name", Spaces 1, Token "String" ]
+ ]
+ let spacesRemoved = removeSpaces filtered
+ it "removeSpaces" $ do
+ spacesRemoved `shouldBe`
+ [ Line { lineIndent = 0, tokens = ["Foo"] }
+ , Line { lineIndent = 2, tokens = ["x", "X"] }
+ , Line { lineIndent = 0, tokens = ["-- | Hello"] }
+ , Line { lineIndent = 0, tokens = ["Bar"] }
+ , Line { lineIndent = 1, tokens = ["name", "String"] }
+ ]
+
+ it "preparse" $ do
+ preparse text `shouldBe`
+ [ Line { lineIndent = 0, tokens = ["Foo"] }
+ , Line { lineIndent = 2, tokens = ["x", "X"] }
+ , Line { lineIndent = 0, tokens = ["-- | Hello"] }
+ , Line { lineIndent = 0, tokens = ["Bar"] }
+ , Line { lineIndent = 1, tokens = ["name", "String"] }
+ ]
+ it "preparse indented" $ do
+ let t = T.unlines
+ [ " Foo"
+ , " x X"
+ , " -- | Comment"
+ , " -- hidden comment"
+ , " Bar"
+ , " name String"
+ ]
+ preparse t `shouldBe`
+ [ Line { lineIndent = 2, tokens = ["Foo"] }
+ , Line { lineIndent = 4, tokens = ["x", "X"] }
+ , Line { lineIndent = 2, tokens = ["-- | Comment"] }
+ , Line { lineIndent = 2, tokens = ["Bar"] }
+ , Line { lineIndent = 4, tokens = ["name", "String"] }
+ ]
+ it "preparse extra blocks" $ do
+ let t = T.unlines
+ [ "LowerCaseTable"
+ , " name String"
+ , " ExtraBlock"
+ , " foo bar"
+ , " baz"
+ , " ExtraBlock2"
+ , " something"
+ ]
+ preparse t `shouldBe`
+ [ Line { lineIndent = 0, tokens = ["LowerCaseTable"] }
+ , Line { lineIndent = 2, tokens = ["name", "String"] }
+ , Line { lineIndent = 2, tokens = ["ExtraBlock"] }
+ , Line { lineIndent = 4, tokens = ["foo", "bar"] }
+ , Line { lineIndent = 4, tokens = ["baz"] }
+ , Line { lineIndent = 2, tokens = ["ExtraBlock2"] }
+ , Line { lineIndent = 4, tokens = ["something"] }
+ ]
+ it "field comments" $ do
+ let text = T.unlines
+ [ "-- | Model"
+ , "Foo"
+ , " -- | Field"
+ , " name String"
+ ]
+ preparse text `shouldBe`
+ [ Line { lineIndent = 0, tokens = ["-- | Model"] }
+ , Line { lineIndent = 0, tokens = ["Foo"] }
+ , Line { lineIndent = 2, tokens = ["-- | Field"] }
+ , Line { lineIndent = 2, tokens = ["name", "String"] }
+ ]
+
+ describe "empty" $ do
+ it "doesn't dispatch comments" $ do
+ [DocComment "-- | hello"] `shouldSatisfy` (not . empty)
+ it "removes spaces" $ do
+ [Spaces 3] `shouldSatisfy` empty
+
+ describe "filter (not . empty)" $ do
+ let subject = filter (not . empty)
+ it "keeps comments" $ do
+ subject [[DocComment "-- | Hello"]]
+ `shouldBe`
+ [[DocComment "-- | Hello"]]
+ it "omits lines with only spaces" $ do
+ subject [[Spaces 3, Token "indented"], [Spaces 2]]
+ `shouldBe`
+ [[Spaces 3, Token "indented"]]
+
+ describe "removeSpaces" $ do
+ it "sets indentation level for a line" $ do
+ removeSpaces [[Spaces 3, Token "hello", Spaces 1, Token "goodbye"]]
+ `shouldBe`
+ [ Line { lineIndent = 3, tokens = ["hello", "goodbye"] }
+ ]
+ it "does not remove comments" $ do
+ removeSpaces
+ [ [ DocComment "-- | asdf" ]
+ , [ Token "Foo" ]
+ , [ Spaces 2, Token "name", Spaces 1, Token "String" ]
+ ]
+ `shouldBe`
+ [ Line { lineIndent = 0, tokens = ["-- | asdf"] }
+ , Line { lineIndent = 0, tokens = ["Foo"] }
+ , Line { lineIndent = 2, tokens = ["name", "String"] }
+ ]
+
+ describe "associateLines" $ do
+ let foo = Line { lineIndent = 0, tokens = pure "Foo" }
+ name'String = Line { lineIndent = 2, tokens = "name" :| ["String"]
}
+ comment = Line { lineIndent = 0, tokens = pure "-- | comment" }
+ it "works" $ do
+ associateLines
+ [ comment
+ , foo
+ , name'String
+ ]
+ `shouldBe`
+ [ LinesWithComments
+ { lwcComments = ["comment"]
+ , lwcLines = foo :| [name'String]
+ }
+ ]
+ let bar = Line { lineIndent = 0, tokens = "Bar" :| ["sql", "=",
"bars"] }
+ age'Int = Line { lineIndent = 1, tokens = "age" :| ["Int"] }
+ it "works when used consecutively" $ do
+ associateLines
+ [ bar
+ , age'Int
+ , comment
+ , foo
+ , name'String
+ ]
+ `shouldBe`
+ [ LinesWithComments
+ { lwcComments = []
+ , lwcLines = bar :| [age'Int]
+ }
+ , LinesWithComments
+ { lwcComments = ["comment"]
+ , lwcLines = foo :| [name'String]
+ }
+ ]
+ it "works with textual input" $ do
+ let text = "Foo\n x X\n-- | Hello\nBar\n name String"
+ parsed = preparse text
+ allFull = skipEmpty parsed
+ associateLines allFull
+ `shouldBe`
+ [ LinesWithComments
+ { lwcLines =
+ Line {lineIndent = 0, tokens = "Foo" :| []}
+ :| [ Line {lineIndent = 2, tokens = "x" :| ["X"]} ]
+ , lwcComments =
+ []
+ }
+ , LinesWithComments
+ { lwcLines =
+ Line {lineIndent = 0, tokens = "Bar" :| []}
+ :| [ Line {lineIndent = 1, tokens = "name" :|
["String"]}]
+ , lwcComments =
+ ["Hello"]
+ }
+ ]
+ it "works with extra blocks" $ do
+ let text = skipEmpty . preparse . T.unlines $
+ [ "LowerCaseTable"
+ , " Id sql=my_id"
+ , " fullName Text"
+ , " ExtraBlock"
+ , " foo bar"
+ , " baz"
+ , " bin"
+ , " ExtraBlock2"
+ , " something"
+ ]
+ associateLines text `shouldBe`
+ [ LinesWithComments
+ { lwcLines =
+ Line { lineIndent = 0, tokens = pure "LowerCaseTable"
} :|
+ [ Line { lineIndent = 4, tokens = "Id" :|
["sql=my_id"] }
+ , Line { lineIndent = 4, tokens = "fullName" :|
["Text"] }
+ , Line { lineIndent = 4, tokens = pure "ExtraBlock" }
+ , Line { lineIndent = 8, tokens = "foo" :| ["bar"] }
+ , Line { lineIndent = 8, tokens = pure "baz" }
+ , Line { lineIndent = 8, tokens = pure "bin" }
+ , Line { lineIndent = 4, tokens = pure "ExtraBlock2" }
+ , Line { lineIndent = 8, tokens = pure "something" }
+ ]
+ , lwcComments = []
+ }
+ ]
+
+ it "works with extra blocks twice" $ do
+ let text = skipEmpty . preparse . T.unlines $
+ [ "IdTable"
+ , " Id Day default=CURRENT_DATE"
+ , " name Text"
+ , ""
+ , "LowerCaseTable"
+ , " Id sql=my_id"
+ , " fullName Text"
+ , " ExtraBlock"
+ , " foo bar"
+ , " baz"
+ , " bin"
+ , " ExtraBlock2"
+ , " something"
+ ]
+ associateLines text `shouldBe`
+ [ LinesWithComments
+ { lwcLines = Line 0 (pure "IdTable") :|
+ [ Line 4 ("Id" :| ["Day", "default=CURRENT_DATE"])
+ , Line 4 ("name" :| ["Text"])
+ ]
+ , lwcComments = []
+ }
+ , LinesWithComments
+ { lwcLines =
+ Line { lineIndent = 0, tokens = pure "LowerCaseTable"
} :|
+ [ Line { lineIndent = 4, tokens = "Id" :|
["sql=my_id"] }
+ , Line { lineIndent = 4, tokens = "fullName" :|
["Text"] }
+ , Line { lineIndent = 4, tokens = pure "ExtraBlock" }
+ , Line { lineIndent = 8, tokens = "foo" :| ["bar"] }
+ , Line { lineIndent = 8, tokens = pure "baz" }
+ , Line { lineIndent = 8, tokens = pure "bin" }
+ , Line { lineIndent = 4, tokens = pure "ExtraBlock2" }
+ , Line { lineIndent = 8, tokens = pure "something" }
+ ]
+ , lwcComments = []
+ }
+ ]
+
+
+ it "works with field comments" $ do
+ let text = skipEmpty . preparse . T.unlines $
+ [ "-- | Model"
+ , "Foo"
+ , " -- | Field"
+ , " name String"
+ ]
+ associateLines text `shouldBe`
+ [ LinesWithComments
+ { lwcLines =
+ Line { lineIndent = 0, tokens = "Foo" :| [] } :|
+ [ Line { lineIndent = 2, tokens = pure "-- |
Field" }
+ , Line { lineIndent = 2, tokens = "name" :|
["String"] }
+ ]
+ , lwcComments =
+ ["Model"]
+ }
+ ]
+
+
+
+ describe "parseLines" $ do
+ let lines =
+ T.unlines
+ [ "-- | Comment"
+ , "Foo"
+ , " -- | Field"
+ , " name String"
+ , " age Int"
+ , " Extra"
+ , " foo bar"
+ , " baz"
+ , " Extra2"
+ , " something"
+ ]
+ let [subject] = parse lowerCaseSettings lines
+ it "produces the right name" $ do
+ entityHaskell subject `shouldBe` HaskellName "Foo"
+ describe "entityFields" $ do
+ let fields = entityFields subject
+ it "has the right field names" $ do
+ map fieldHaskell fields `shouldMatchList`
+ [ HaskellName "name"
+ , HaskellName "age"
+ ]
+ it "has comments" $ do
+ map fieldComments fields `shouldBe`
+ [ Just "Field\n"
+ , Nothing
+ ]
+ it "has the comments" $ do
+ entityComments subject `shouldBe`
+ Just "Comment\n"
+ it "combines extrablocks" $ do
+ entityExtra subject `shouldBe` Map.fromList
+ [ ("Extra", [["foo", "bar"], ["baz"]])
+ , ("Extra2", [["something"]])
+ ]
+ describe "works with extra blocks" $ do
+ let [_, lowerCaseTable, idTable] =
+ parse lowerCaseSettings $ T.unlines
+ [ ""
+ , "IdTable"
+ , " Id Day default=CURRENT_DATE"
+ , " name Text"
+ , ""
+ , "LowerCaseTable"
+ , " Id sql=my_id"
+ , " fullName Text"
+ , " ExtraBlock"
+ , " foo bar"
+ , " baz"
+ , " bin"
+ , " ExtraBlock2"
+ , " something"
+ , ""
+ , "IdTable"
+ , " Id Day default=CURRENT_DATE"
+ , " name Text"
+ , ""
+ ]
+ describe "idTable" $ do
+ let EntityDef {..} = idTable
+ it "has no extra blocks" $ do
+ entityExtra `shouldBe` mempty
+ it "has the right name" $ do
+ entityHaskell `shouldBe` HaskellName "IdTable"
+ it "has the right fields" $ do
+ map fieldHaskell entityFields `shouldMatchList`
+ [ HaskellName "name"
+ ]
+ describe "lowerCaseTable" $ do
+ let EntityDef {..} = lowerCaseTable
+ it "has the right name" $ do
+ entityHaskell `shouldBe` HaskellName "LowerCaseTable"
+ it "has the right fields" $ do
+ map fieldHaskell entityFields `shouldMatchList`
+ [ HaskellName "fullName"
+ ]
+ it "has ExtraBlock" $ do
+ Map.lookup "ExtraBlock" entityExtra
+ `shouldBe` Just
+ [ ["foo", "bar"]
+ , ["baz"]
+ , ["bin"]
+ ]
+ it "has ExtraBlock2" $ do
+ Map.lookup "ExtraBlock2" entityExtra
+ `shouldBe` Just
+ [ ["something"]
+ ]
+