Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Postgresql constraint= attribute support #979

Merged
merged 10 commits into from
Oct 31, 2019
4 changes: 4 additions & 0 deletions persistent-postgresql/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog for persistent-postgresql

## 2.10.1

* Added support for the `constraint=` attribute to the Postgresql backend. [#979](https://github.com/yesodweb/persistent/pull/979)

## 2.10.0

* Added question mark operators (`(?.), (?|.), (?&.)`) to `Database.Persist.Postgresql.JSON` [#863](https://github.com/yesodweb/persistent/pull/863)
Expand Down
51 changes: 29 additions & 22 deletions persistent-postgresql/Database/Persist/Postgresql.hs
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ migrate' allDefs getter entity = fmap (fmap $ map showAlterDb) $ do
migrationText exists old'' =
if not exists
then createText newcols fdefs udspair
else let (acs, ats) = getAlters allDefs entity (newcols, udspair) old'
else let (acs, ats) = getAlters allDefs entity (newcols, udspair) $ excludeForeignKeys $ old'
acs' = map (AlterColumn name) acs
ats' = map (AlterTable name) ats
in acs' ++ ats'
Expand All @@ -577,6 +577,13 @@ migrate' allDefs getter entity = fmap (fmap $ map showAlterDb) $ do
(newcols', udefs, fdefs) = mkColumns allDefs entity
newcols = filter (not . safeToRemove entity . cName) newcols'
udspair = map udToPair udefs
excludeForeignKeys (xs,ys) = (map excludeForeignKey xs,ys)
excludeForeignKey c = case cReference c of
Just (_,fk) ->
case find (\f -> fk == foreignConstraintNameDBName f) fdefs of
Just _ -> c { cReference = Nothing }
Nothing -> c
Nothing -> c
-- Check for table existence if there are no columns, workaround
-- for https://github.com/yesodweb/persistent/issues/152

Expand Down Expand Up @@ -792,23 +799,27 @@ getColumn getter tableName' [PersistText columnName, PersistText isNullable, Per
Nothing -> loop' ps
Just t' -> t'
getRef cname = do
let sql = T.concat
[ "SELECT COUNT(*) FROM "
, "information_schema.table_constraints "
, "WHERE table_catalog=current_database() "
, "AND table_schema=current_schema() "
, "AND table_name=? "
, "AND constraint_type='FOREIGN KEY' "
, "AND constraint_name=?"
]
let ref = refName tableName' cname
let sql = T.concat ["SELECT DISTINCT "
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the way postgresql stores the column metadata, I had to useDISTINCT here in order to avoid duplicate rows being returned for composite keys.

,"ccu.table_name, "
,"tc.constraint_name "
,"FROM information_schema.constraint_column_usage ccu, "
,"information_schema.key_column_usage kcu, "
,"information_schema.table_constraints tc "
,"WHERE tc.constraint_type='FOREIGN KEY' "
,"AND kcu.constraint_name=tc.constraint_name "
,"AND ccu.constraint_name=kcu.constraint_name "
,"AND kcu.ordinal_position=1 "
,"AND kcu.table_name=? "
,"AND kcu.column_name=?"]
stmt <- getter sql
with (stmtQuery stmt
[ PersistText $ unDBName tableName'
, PersistText $ unDBName ref
]) (\src -> runConduit $ src .| do
Just [PersistInt64 i] <- CL.head
return $ if i == 0 then Nothing else Just (DBName "", ref))
cntrs <- with (stmtQuery stmt [PersistText $ unDBName tableName'
,PersistText $ unDBName cname])
(\src -> runConduit $ src .| CL.consume)
case cntrs of
[] -> return Nothing
[[PersistText table, PersistText constraint]] ->
return $ Just (DBName table, DBName constraint)
_ -> error "Postgresql.getColumn: error fetching constraints"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robbassi So this is the place where the error is thrown. Is there a reason you assume a single constraint per table here and throw the error? A simple idea of a schema that this breaks is a table that records sales like:

Sales 
  buyer PersonId 
  seller PersonId

Person
  name Text

d' = case defaultValue of
PersistNull -> Right Nothing
PersistText t -> Right $ Just t
Expand Down Expand Up @@ -908,7 +919,7 @@ getAddReference :: [EntityDef] -> DBName -> DBName -> DBName -> Maybe (DBName, D
getAddReference allDefs table reftable cname ref =
case ref of
Nothing -> Nothing
Just (s, _) -> Just $ AlterColumn table (s, AddReference (refName table cname) [cname] id_)
Just (s, constraintName) -> Just $ AlterColumn table (s, AddReference constraintName [cname] id_)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

where
id_ = fromMaybe (error $ "Could not find ID of entity " ++ show reftable)
$ do
Expand Down Expand Up @@ -1140,10 +1151,6 @@ instance PersistConfig PostgresConf where
addPass = maybeAddParam "password" "PGPASS"
addDatabase = maybeAddParam "dbname" "PGDATABASE"

refName :: DBName -> DBName -> DBName
refName (DBName table) (DBName column) =
DBName $ T.concat [table, "_", column, "_fkey"]

udToPair :: UniqueDef -> (DBName, [DBName])
udToPair ud = (uniqueDBName ud, map snd $ uniqueFields ud)

Expand Down
3 changes: 2 additions & 1 deletion persistent-postgresql/persistent-postgresql.cabal
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: persistent-postgresql
version: 2.10.0
version: 2.10.1
license: MIT
license-file: LICENSE
author: Felipe Lessa, Michael Snoyman <michael@snoyman.com>
Expand Down Expand Up @@ -48,6 +48,7 @@ test-suite test
ArrayAggTest
EquivalentTypeTestPostgres
JSONTest
CustomConstraintTest
ghc-options: -Wall

build-depends: base >= 4.9 && < 5
Expand Down
51 changes: 51 additions & 0 deletions persistent-postgresql/test/CustomConstraintTest.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module CustomConstraintTest where

import PgInit
import qualified Data.Text as T

share [mkPersist sqlSettings, mkMigrate "customConstraintMigrate"] [persistLowerCase|
CustomConstraint1
some_field Text
deriving Show

CustomConstraint2
cc_id CustomConstraint1Id constraint=custom_constraint
deriving Show
|]

specs :: (MonadIO m, MonadFail m) => RunDb SqlBackend m -> Spec
specs runDb = do
describe "custom constraint used in migration" $ do
it "custom constraint is actually created" $ runDb $ do
runMigration customConstraintMigrate
runMigration customConstraintMigrate -- run a second time to ensure the constraint isn't dropped
let query = T.concat ["SELECT DISTINCT COUNT(*) "
,"FROM information_schema.constraint_column_usage ccu, "
,"information_schema.key_column_usage kcu, "
,"information_schema.table_constraints tc "
,"WHERE tc.constraint_type='FOREIGN KEY' "
,"AND kcu.constraint_name=tc.constraint_name "
,"AND ccu.constraint_name=kcu.constraint_name "
,"AND kcu.ordinal_position=1 "
,"AND ccu.table_name=? "
,"AND ccu.column_name=? "
,"AND kcu.table_name=? "
,"AND kcu.column_name=? "
,"AND tc.constraint_name=?"]
[Single exists] <- rawSql query [PersistText "custom_constraint1"
,PersistText "id"
,PersistText "custom_constraint2"
,PersistText "cc_id"
,PersistText "custom_constraint"]
liftIO $ 1 @?= (exists :: Int)

2 changes: 2 additions & 0 deletions persistent-postgresql/test/main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import qualified TransactionLevelTest
import qualified TreeTest
import qualified UniqueTest
import qualified UpsertTest
import qualified CustomConstraintTest

type Tuple = (,)

Expand Down Expand Up @@ -172,5 +173,6 @@ main = do
EquivalentTypeTestPostgres.specs
TransactionLevelTest.specsWith db
JSONTest.specs
CustomConstraintTest.specs db
-- FIXME: not used, probably should?
-- ArrayAggTest.specs db