refactor: PKcols in table, ViewKeyDependency type
* Get PKcols inside tables - done with SQL for tables and with an additional step in Haskell for views. This fixes an fk column being considered as a pk column on views and corrects the test added on https://github.com/PostgREST/postgrest/pull/1875/files/1d549768580310e18aac4ffa6dbd01c5b77934a7#r853674126 * classify view key dependencies in SQL * remove Column from Relationship * Merge cols/fcols in Relationship and ensure allM2ORels and allViewsKeyDependencies fk columns are ordered - done by attnum in SQL * Cardinality now contains relColumns instead of Relationship - this simplifies getJoinConditions.
This commit is contained in:
@@ -48,6 +48,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #2147, Ignore `Content-Type` headers for `GET` requests when calling RPCs. Previously, `GET` without parameters, but with `Content-Type: text/plain` or `Content-Type: application/octet-stream` would fail with `404 Not Found`, even if a function without arguments was available.
|
- #2147, Ignore `Content-Type` headers for `GET` requests when calling RPCs. Previously, `GET` without parameters, but with `Content-Type: text/plain` or `Content-Type: application/octet-stream` would fail with `404 Not Found`, even if a function without arguments was available.
|
||||||
- #2155, Ignore `max-rows` on POST, PATCH, PUT and DELETE - @steve-chavez
|
- #2155, Ignore `max-rows` on POST, PATCH, PUT and DELETE - @steve-chavez
|
||||||
- #2239, Fix misleading disambiguation error where the content of the `relationship` key looks like valid syntax - @laurenceisla
|
- #2239, Fix misleading disambiguation error where the content of the `relationship` key looks like valid syntax - @laurenceisla
|
||||||
|
- #2254, Fix inferring a foreign key column as a primary key column on views - @steve-chavez
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import PostgREST.Config (AppConfig (..),
|
|||||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||||
import PostgREST.ContentType (ContentType (..))
|
import PostgREST.ContentType (ContentType (..))
|
||||||
import PostgREST.DbStructure (DbStructure (..),
|
import PostgREST.DbStructure (DbStructure (..),
|
||||||
findIfView, tablePKCols)
|
findIfView)
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
@@ -313,7 +313,7 @@ handleCreate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
|||||||
handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
|
handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
|
||||||
let
|
let
|
||||||
ApiRequest{..} = ctxApiRequest
|
ApiRequest{..} = ctxApiRequest
|
||||||
pkCols = tablePKCols ctxDbStructure qiSchema qiName
|
pkCols = maybe mempty tablePKCols $ M.lookup identifier $ dbTables ctxDbStructure
|
||||||
|
|
||||||
WriteQueryResult{..} <- writeQuery MutationCreate identifier True pkCols context
|
WriteQueryResult{..} <- writeQuery MutationCreate identifier True pkCols context
|
||||||
|
|
||||||
@@ -433,7 +433,7 @@ handleInfo identifier RequestContext{..} =
|
|||||||
++ ["DELETE" | tableDeletable table]
|
++ ["DELETE" | tableDeletable table]
|
||||||
)
|
)
|
||||||
hasPK =
|
hasPK =
|
||||||
not $ null $ tablePKCols ctxDbStructure (qiSchema identifier) (qiName identifier)
|
not $ null $ maybe mempty tablePKCols $ M.lookup identifier (dbTables ctxDbStructure)
|
||||||
|
|
||||||
handleInvoke :: InvokeMethod -> ProcDescription -> RequestContext -> DbHandler Wai.Response
|
handleInvoke :: InvokeMethod -> ProcDescription -> RequestContext -> DbHandler Wai.Response
|
||||||
handleInvoke invMethod proc context@RequestContext{..} = do
|
handleInvoke invMethod proc context@RequestContext{..} = do
|
||||||
@@ -537,7 +537,7 @@ writeQuery mutation identifier@QualifiedIdentifier{..} isInsert pkCols context@R
|
|||||||
mutateReq <-
|
mutateReq <-
|
||||||
liftEither $
|
liftEither $
|
||||||
ReqBuilder.mutateRequest mutation qiSchema qiName ctxApiRequest
|
ReqBuilder.mutateRequest mutation qiSchema qiName ctxApiRequest
|
||||||
(tablePKCols ctxDbStructure qiSchema qiName)
|
(maybe mempty tablePKCols $ M.lookup identifier $ dbTables ctxDbStructure)
|
||||||
readReq
|
readReq
|
||||||
|
|
||||||
(_, queryTotal, fields, body, gucHeaders, gucStatus) <-
|
(_, queryTotal, fields, body, gucHeaders, gucStatus) <-
|
||||||
|
|||||||
+201
-227
@@ -26,26 +26,24 @@ module PostgREST.DbStructure
|
|||||||
, findIfView
|
, findIfView
|
||||||
, schemaDescription
|
, schemaDescription
|
||||||
, tableCols
|
, tableCols
|
||||||
, tablePKCols
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.List as L
|
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
import qualified Hasql.Statement as SQL
|
import qualified Hasql.Statement as SQL
|
||||||
import qualified Hasql.Transaction as SQL
|
import qualified Hasql.Transaction as SQL
|
||||||
|
|
||||||
import Contravariant.Extras (contrazip2)
|
import Contravariant.Extras (contrazip2)
|
||||||
import Data.Set as S (fromList)
|
|
||||||
import Data.Text (split)
|
import Data.Text (split)
|
||||||
import Text.InterpolatedString.Perl6 (q)
|
import Text.InterpolatedString.Perl6 (q)
|
||||||
|
|
||||||
import PostgREST.Config.Database (pgVersionStatement)
|
import PostgREST.Config.Database (pgVersionStatement)
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||||
pgVersion110)
|
pgVersion110)
|
||||||
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier (..),
|
||||||
Schema, TableName)
|
Schema, TableName)
|
||||||
import PostgREST.DbStructure.Proc (PgType (..),
|
import PostgREST.DbStructure.Proc (PgType (..),
|
||||||
ProcDescription (..),
|
ProcDescription (..),
|
||||||
@@ -54,20 +52,17 @@ import PostgREST.DbStructure.Proc (PgType (..),
|
|||||||
ProcsMap, RetType (..))
|
ProcsMap, RetType (..))
|
||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
PrimaryKey (..),
|
|
||||||
Relationship (..))
|
Relationship (..))
|
||||||
import PostgREST.DbStructure.Table (Column (..), Table (..),
|
import PostgREST.DbStructure.Table (Column (..), Table (..),
|
||||||
TablesMap)
|
TablesMap)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
import Protolude.Unsafe (unsafeHead)
|
|
||||||
|
|
||||||
|
|
||||||
data DbStructure = DbStructure
|
data DbStructure = DbStructure
|
||||||
{ dbTables :: TablesMap
|
{ dbTables :: TablesMap
|
||||||
, dbColumns :: [Column]
|
, dbColumns :: [Column]
|
||||||
, dbRelationships :: [Relationship]
|
, dbRelationships :: [Relationship]
|
||||||
, dbPrimaryKeys :: [PrimaryKey]
|
|
||||||
, dbProcs :: ProcsMap
|
, dbProcs :: ProcsMap
|
||||||
}
|
}
|
||||||
deriving (Generic, JSON.ToJSON)
|
deriving (Generic, JSON.ToJSON)
|
||||||
@@ -76,16 +71,22 @@ data DbStructure = DbStructure
|
|||||||
tableCols :: DbStructure -> Schema -> TableName -> [Column]
|
tableCols :: DbStructure -> Schema -> TableName -> [Column]
|
||||||
tableCols dbs tSchema tName = filter (\Column{colTable=Table{tableSchema=s, tableName=t}} -> s==tSchema && t==tName) $ dbColumns dbs
|
tableCols dbs tSchema tName = filter (\Column{colTable=Table{tableSchema=s, tableName=t}} -> s==tSchema && t==tName) $ dbColumns dbs
|
||||||
|
|
||||||
-- TODO Table could hold references to all its PrimaryKeys
|
|
||||||
tablePKCols :: DbStructure -> Schema -> TableName -> [Text]
|
|
||||||
tablePKCols dbs tSchema tName = pkName <$> filter (\pk -> tSchema == (tableSchema . pkTable) pk && tName == (tableName . pkTable) pk) (dbPrimaryKeys dbs)
|
|
||||||
|
|
||||||
findIfView :: QualifiedIdentifier -> TablesMap -> Bool
|
findIfView :: QualifiedIdentifier -> TablesMap -> Bool
|
||||||
findIfView identifier tbls = maybe False tableIsView $ M.lookup identifier tbls
|
findIfView identifier tbls = maybe False tableIsView $ M.lookup identifier tbls
|
||||||
|
|
||||||
-- | The source table column a view column refers to
|
-- | A view foreign key or primary key dependency detected on its source table
|
||||||
type SourceColumn = (Column, ViewColumn)
|
data ViewKeyDependency = ViewKeyDependency {
|
||||||
type ViewColumn = Column
|
keyDepTable :: QualifiedIdentifier
|
||||||
|
, keyDepView :: QualifiedIdentifier
|
||||||
|
, keyDepCons :: Text
|
||||||
|
, keyDepType :: KeyDep
|
||||||
|
, keyDepCols :: [(FieldName, FieldName)] -- ^ First element is the table column, second is the view column
|
||||||
|
} deriving (Eq)
|
||||||
|
data KeyDep
|
||||||
|
= PKDep -- ^ PK dependency
|
||||||
|
| FKDep -- ^ FK dependency
|
||||||
|
| FKDepRef -- ^ FK reference dependency
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
-- | A SQL query that can be executed independently
|
-- | A SQL query that can be executed independently
|
||||||
type SqlQuery = ByteString
|
type SqlQuery = ByteString
|
||||||
@@ -96,19 +97,17 @@ queryDbStructure schemas extraSearchPath prepared = do
|
|||||||
pgVer <- SQL.statement mempty pgVersionStatement
|
pgVer <- SQL.statement mempty pgVersionStatement
|
||||||
tabs <- SQL.statement mempty $ allTables pgVer prepared
|
tabs <- SQL.statement mempty $ allTables pgVer prepared
|
||||||
cols <- SQL.statement schemas $ allColumns tabs prepared
|
cols <- SQL.statement schemas $ allColumns tabs prepared
|
||||||
srcCols <- SQL.statement (schemas, extraSearchPath) $ pfkSourceColumns cols prepared
|
keyDeps <- SQL.statement (schemas, extraSearchPath) $ allViewsKeyDependencies prepared
|
||||||
m2oRels <- SQL.statement mempty $ allM2ORels pgVer cols prepared
|
m2oRels <- SQL.statement mempty $ allM2ORels pgVer prepared
|
||||||
keys <- SQL.statement mempty $ allPrimaryKeys tabs prepared
|
|
||||||
procs <- SQL.statement schemas $ allProcs pgVer prepared
|
procs <- SQL.statement schemas $ allProcs pgVer prepared
|
||||||
|
|
||||||
let rels = addO2MRels . addM2MRels $ addViewM2ORels srcCols m2oRels
|
let rels = addO2MRels $ addM2MRels $ addViewM2ORels keyDeps m2oRels
|
||||||
keys' = addViewPrimaryKeys srcCols keys
|
tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
|
||||||
|
|
||||||
return $ removeInternal schemas $ DbStructure {
|
return $ removeInternal schemas $ DbStructure {
|
||||||
dbTables = tabs
|
dbTables = tabsWViewsPks
|
||||||
, dbColumns = cols
|
, dbColumns = cols
|
||||||
, dbRelationships = rels
|
, dbRelationships = rels
|
||||||
, dbPrimaryKeys = keys'
|
|
||||||
, dbProcs = procs
|
, dbProcs = procs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +120,6 @@ removeInternal schemas dbStruct =
|
|||||||
, dbRelationships = filter (\x -> qiSchema (relTable x) `elem` schemas &&
|
, dbRelationships = filter (\x -> qiSchema (relTable x) `elem` schemas &&
|
||||||
qiSchema (relForeignTable x) `elem` schemas &&
|
qiSchema (relForeignTable x) `elem` schemas &&
|
||||||
not (hasInternalJunction x)) $ dbRelationships dbStruct
|
not (hasInternalJunction x)) $ dbRelationships dbStruct
|
||||||
, dbPrimaryKeys = filter (\x -> tableSchema (pkTable x) `elem` schemas) $ dbPrimaryKeys dbStruct
|
|
||||||
, dbProcs = dbProcs dbStruct -- procs are only obtained from the exposed schemas, no need to filter them.
|
, dbProcs = dbProcs dbStruct -- procs are only obtained from the exposed schemas, no need to filter them.
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
@@ -140,6 +138,7 @@ decodeTables =
|
|||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
|
<*> arrayColumn HD.text
|
||||||
|
|
||||||
decodeColumns :: TablesMap -> HD.Result [Column]
|
decodeColumns :: TablesMap -> HD.Result [Column]
|
||||||
decodeColumns tables =
|
decodeColumns tables =
|
||||||
@@ -157,40 +156,42 @@ decodeColumns tables =
|
|||||||
<*> nullableColumn HD.text
|
<*> nullableColumn HD.text
|
||||||
<*> nullableColumn HD.text
|
<*> nullableColumn HD.text
|
||||||
|
|
||||||
decodeRels :: [Column] -> HD.Result [Relationship]
|
decodeRels :: HD.Result [Relationship]
|
||||||
decodeRels cols =
|
decodeRels =
|
||||||
mapMaybe (relFromRow cols) <$> HD.rowList relRow
|
map relFromRow <$> HD.rowList relRow
|
||||||
where
|
where
|
||||||
relRow = (,,,,,,)
|
relRow = (,,,,,)
|
||||||
<$> column HD.text
|
<$> column HD.text <*> column HD.text
|
||||||
|
<*> column HD.text <*> column HD.text
|
||||||
<*> column HD.text
|
<*> column HD.text
|
||||||
<*> column HD.text
|
<*> compositeArrayColumn
|
||||||
<*> arrayColumn HD.text
|
((,)
|
||||||
<*> column HD.text
|
<$> compositeField HD.text
|
||||||
<*> column HD.text
|
<*> compositeField HD.text)
|
||||||
<*> arrayColumn HD.text
|
|
||||||
|
|
||||||
decodePks :: TablesMap -> HD.Result [PrimaryKey]
|
relFromRow :: (Text, Text, Text, Text, Text, [(Text, Text)]) -> Relationship
|
||||||
decodePks tables =
|
relFromRow (rs, rt, frs, frt, cn, cs) =
|
||||||
mapMaybe (pkFromRow tables) <$> HD.rowList pkRow
|
Relationship (QualifiedIdentifier rs rt) (QualifiedIdentifier frs frt) (M2O cn cs)
|
||||||
where
|
|
||||||
pkRow = (,,) <$> column HD.text <*> column HD.text <*> column HD.text
|
|
||||||
|
|
||||||
decodeSourceColumns :: [Column] -> HD.Result [SourceColumn]
|
decodeViewKeyDeps :: HD.Result [ViewKeyDependency]
|
||||||
decodeSourceColumns cols =
|
decodeViewKeyDeps =
|
||||||
mapMaybe (sourceColumnFromRow cols) <$> HD.rowList srcColRow
|
map viewKeyDepFromRow <$> HD.rowList row
|
||||||
where
|
where
|
||||||
srcColRow = (,,,,,)
|
row = (,,,,,,)
|
||||||
<$> column HD.text <*> column HD.text
|
<$> column HD.text <*> column HD.text
|
||||||
<*> column HD.text <*> column HD.text
|
<*> column HD.text <*> column HD.text
|
||||||
<*> column HD.text <*> column HD.text
|
<*> column HD.text <*> column HD.text
|
||||||
|
<*> compositeArrayColumn
|
||||||
|
((,)
|
||||||
|
<$> compositeField HD.text
|
||||||
|
<*> compositeField HD.text)
|
||||||
|
|
||||||
sourceColumnFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe SourceColumn
|
viewKeyDepFromRow :: (Text,Text,Text,Text,Text,Text,[(Text, Text)]) -> ViewKeyDependency
|
||||||
sourceColumnFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
|
viewKeyDepFromRow (s1,t1,s2,v2,cons,consType,sCols) = ViewKeyDependency (QualifiedIdentifier s1 t1) (QualifiedIdentifier s2 v2) cons keyDep sCols
|
||||||
where
|
where
|
||||||
col1 = findCol s1 t1 c1
|
keyDep | consType == "p" = PKDep
|
||||||
col2 = findCol s2 t2 c2
|
| consType == "f" = FKDep
|
||||||
findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols
|
| otherwise = FKDepRef -- f_ref, we build this type in the query
|
||||||
|
|
||||||
decodeProcs :: HD.Result ProcsMap
|
decodeProcs :: HD.Result ProcsMap
|
||||||
decodeProcs =
|
decodeProcs =
|
||||||
@@ -337,91 +338,71 @@ accessibleTables pgVer =
|
|||||||
sql = tablesSqlQuery False pgVer
|
sql = tablesSqlQuery False pgVer
|
||||||
|
|
||||||
{-
|
{-
|
||||||
Adds Views M2O Relationships based on SourceColumns found, the logic is as follows:
|
Adds M2O relationships for views to tables, tables to views, and views to views. The example below is taken from the test fixtures, but the views names/colnames were modified.
|
||||||
|
|
||||||
Having a Relationship{relTable=t1, relColumns=[c1], relFTable=t2, relFColumns=[c2], relCardinality=M2O} represented by:
|
--allM2ORels sample query result--
|
||||||
|
private | personnages | private | actors | personnages_role_id_fkey | {"(role_id,id)"}
|
||||||
|
|
||||||
t1.c1------t2.c2
|
--allViewsKeyDependencies sample query result--
|
||||||
|
private | personnages | test | personnages_view | personnages_role_id_fkey | f | {"(role_id,roleId)"}
|
||||||
|
private | actors | test | actors_view | personnages_role_id_fkey | f_ref | {"(id,actorId)"}
|
||||||
|
|
||||||
When only having a t1_view.c1 source column, we need to add a View-Table M2O Relationship
|
--this function result--
|
||||||
|
test | personnages_view | private | actors | personnages_role_id_fkey | f | {"(roleId,id)"} | viewTableM2O
|
||||||
t1.c1----t2.c2 t1.c1----------t2.c2
|
private | personnages | test | actors_view | personnages_role_id_fkey | f_ref | {"(role_id,actorId)"} | tableViewM2O
|
||||||
-> ________/
|
test | personnages_view | test | actors_view | personnages_role_id_fkey | f,r_ref | {"(roleId,actorId)"} | viewViewM2O
|
||||||
/
|
|
||||||
t1_view.c1 t1_view.c1
|
|
||||||
|
|
||||||
|
|
||||||
When only having a t2_view.c2 source column, we need to add a Table-View M2O Relationship
|
|
||||||
|
|
||||||
t1.c1----t2.c2 t1.c1----------t2.c2
|
|
||||||
-> \________
|
|
||||||
\
|
|
||||||
t2_view.c2 t2_view.c1
|
|
||||||
|
|
||||||
When having t1_view.c1 and a t2_view.c2 source columns, we need to add a View-View M2O Relationship in addition to the prior
|
|
||||||
|
|
||||||
t1.c1----t2.c2 t1.c1----------t2.c2
|
|
||||||
-> \________/
|
|
||||||
/ \
|
|
||||||
t1_view.c1 t2_view.c2 t1_view.c1-------t2_view.c1
|
|
||||||
|
|
||||||
The logic for composite pks is similar just need to make sure all the Relationship columns have source columns.
|
|
||||||
-}
|
-}
|
||||||
addViewM2ORels :: [SourceColumn] -> [Relationship] -> [Relationship]
|
addViewM2ORels :: [ViewKeyDependency] -> [Relationship] -> [Relationship]
|
||||||
addViewM2ORels allSrcCols = concatMap (\rel@Relationship{..} -> rel :
|
addViewM2ORels keyDeps rels =
|
||||||
let
|
rels ++ concat (viewRels <$> rels)
|
||||||
srcColsGroupedByView :: [Column] -> [[SourceColumn]]
|
where
|
||||||
srcColsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $
|
viewRels Relationship{relTable,relForeignTable,relCardinality=M2O cons relColumns} =
|
||||||
filter (\(c, _) -> c `elem` relCols) allSrcCols
|
let
|
||||||
relSrcCols = srcColsGroupedByView relColumns
|
viewTableM2Os = filter (\ViewKeyDependency{keyDepTable, keyDepCons, keyDepType} -> keyDepTable == relTable && keyDepCons == cons && keyDepType == FKDep) keyDeps
|
||||||
relFSrcCols = srcColsGroupedByView relForeignColumns
|
tableViewM2Os = filter (\ViewKeyDependency{keyDepTable, keyDepCons, keyDepType} -> keyDepTable == relForeignTable && keyDepCons == cons && keyDepType == FKDepRef) keyDeps
|
||||||
getView :: [SourceColumn] -> QualifiedIdentifier
|
in
|
||||||
getView = (\t -> QualifiedIdentifier (tableSchema t) (tableName t)) . colTable . snd . unsafeHead
|
[ Relationship
|
||||||
srcCols `allSrcColsOf` cols = S.fromList (fst <$> srcCols) == S.fromList cols
|
(keyDepView vwTbl)
|
||||||
-- Relationship is dependent on the order of relColumns and relFColumns to get the join conditions right in the generated query.
|
relForeignTable
|
||||||
-- So we need to change the order of the SourceColumns to match the relColumns
|
(M2O cons $ zipWith (\(_, vCol) (_, fCol)-> (vCol, fCol)) (keyDepCols vwTbl) relColumns)
|
||||||
-- TODO: This could be avoided if the Relationship type is improved with a structure that maintains the association of relColumns and relFColumns
|
| vwTbl <- viewTableM2Os ]
|
||||||
srcCols `sortAccordingTo` cols = sortOn (\(k, _) -> L.lookup k $ zip cols [0::Int ..]) srcCols
|
++
|
||||||
|
[ Relationship
|
||||||
|
relTable
|
||||||
|
(keyDepView tblVw)
|
||||||
|
(M2O cons $ zipWith (\(tCol, _) (_, vCol) -> (tCol, vCol)) relColumns (keyDepCols tblVw))
|
||||||
|
| tblVw <- tableViewM2Os ]
|
||||||
|
++
|
||||||
|
[ Relationship
|
||||||
|
(keyDepView vwTbl)
|
||||||
|
(keyDepView tblVw)
|
||||||
|
(M2O cons $ zipWith (\(_, vcol1) (_, vcol2) -> (vcol1, vcol2)) (keyDepCols vwTbl) (keyDepCols tblVw))
|
||||||
|
| vwTbl <- viewTableM2Os
|
||||||
|
, tblVw <- tableViewM2Os ]
|
||||||
|
viewRels _ = []
|
||||||
|
|
||||||
viewTableM2O =
|
|
||||||
[ Relationship
|
|
||||||
(getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
|
||||||
relForeignTable relForeignColumns relCardinality
|
|
||||||
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ]
|
|
||||||
|
|
||||||
tableViewM2O =
|
|
||||||
[ Relationship
|
|
||||||
relTable relColumns
|
|
||||||
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relForeignColumns)
|
|
||||||
relCardinality
|
|
||||||
| fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relForeignColumns ]
|
|
||||||
|
|
||||||
viewViewM2O =
|
|
||||||
[ Relationship
|
|
||||||
(getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
|
||||||
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relForeignColumns)
|
|
||||||
relCardinality
|
|
||||||
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns
|
|
||||||
, fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relForeignColumns ]
|
|
||||||
|
|
||||||
in viewTableM2O ++ tableViewM2O ++ viewViewM2O)
|
|
||||||
|
|
||||||
addO2MRels :: [Relationship] -> [Relationship]
|
addO2MRels :: [Relationship] -> [Relationship]
|
||||||
addO2MRels rels = rels ++ [ Relationship ft fc t c (O2M cons)
|
addO2MRels rels = rels ++ [ Relationship ft t (O2M cons (swap <$> cols))
|
||||||
| Relationship t c ft fc (M2O cons) <- rels ]
|
| Relationship t ft (M2O cons cols) <- rels ]
|
||||||
|
|
||||||
addM2MRels :: [Relationship] -> [Relationship]
|
addM2MRels :: [Relationship] -> [Relationship]
|
||||||
addM2MRels rels = rels ++ [ Relationship t c ft fc (M2M $ Junction jt1 cons1 jc1 cons2 jc2)
|
addM2MRels rels = rels ++ [ Relationship t ft
|
||||||
| Relationship jt1 jc1 t c (M2O cons1) <- rels
|
(M2M $ Junction jt1 cons1 cons2 (swap <$> cols) (swap <$> fcols))
|
||||||
, Relationship jt2 jc2 ft fc (M2O cons2) <- rels
|
| Relationship jt1 t (M2O cons1 cols) <- rels
|
||||||
|
, Relationship jt2 ft (M2O cons2 fcols) <- rels
|
||||||
, jt1 == jt2
|
, jt1 == jt2
|
||||||
, cons1 /= cons2]
|
, cons1 /= cons2]
|
||||||
|
|
||||||
addViewPrimaryKeys :: [SourceColumn] -> [PrimaryKey] -> [PrimaryKey]
|
addViewPrimaryKeys :: TablesMap -> [ViewKeyDependency] -> TablesMap
|
||||||
addViewPrimaryKeys srcCols = concatMap (\pk ->
|
addViewPrimaryKeys tabs keyDeps =
|
||||||
let viewPks = (\(_, viewCol) -> PrimaryKey{pkTable=colTable viewCol, pkName=colName viewCol}) <$>
|
(\tbl@Table{tableSchema, tableName, tableIsView}-> if tableIsView
|
||||||
filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) srcCols in
|
then tbl{tablePKCols=findViewPKCols tableSchema tableName}
|
||||||
pk : viewPks)
|
else tbl) <$> tabs
|
||||||
|
where
|
||||||
|
findViewPKCols sch vw =
|
||||||
|
maybe [] (\(ViewKeyDependency _ _ _ _ pkCols) -> snd <$> pkCols) $
|
||||||
|
find (\(ViewKeyDependency _ viewQi _ dep _) -> dep == PKDep && viewQi == QualifiedIdentifier sch vw) keyDeps
|
||||||
|
|
||||||
allTables :: PgVersion -> Bool -> SQL.Statement () TablesMap
|
allTables :: PgVersion -> Bool -> SQL.Statement () TablesMap
|
||||||
allTables pgVer =
|
allTables pgVer =
|
||||||
@@ -429,8 +410,84 @@ allTables pgVer =
|
|||||||
where
|
where
|
||||||
sql = tablesSqlQuery True pgVer
|
sql = tablesSqlQuery True pgVer
|
||||||
|
|
||||||
|
-- | Gets tables with their PK cols
|
||||||
tablesSqlQuery :: Bool -> PgVersion -> SqlQuery
|
tablesSqlQuery :: Bool -> PgVersion -> SqlQuery
|
||||||
tablesSqlQuery getAll pgVer = [q|
|
tablesSqlQuery getAll pgVer =
|
||||||
|
-- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views,
|
||||||
|
-- we cannot use those directly as they include the following privilege filter:
|
||||||
|
-- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text));
|
||||||
|
[q|
|
||||||
|
WITH
|
||||||
|
tbl_constraints AS (
|
||||||
|
SELECT
|
||||||
|
c.conname::name AS constraint_name,
|
||||||
|
nr.nspname::name AS table_schema,
|
||||||
|
r.relname::name AS table_name
|
||||||
|
FROM pg_namespace nc
|
||||||
|
JOIN pg_constraint c ON nc.oid = c.connamespace
|
||||||
|
JOIN pg_class r ON c.conrelid = r.oid
|
||||||
|
JOIN pg_namespace nr ON nr.oid = r.relnamespace
|
||||||
|
WHERE
|
||||||
|
r.relkind IN ('r', 'p')
|
||||||
|
AND NOT pg_is_other_temp_schema(nr.oid)
|
||||||
|
AND c.contype = 'p'
|
||||||
|
),
|
||||||
|
key_col_usage AS (
|
||||||
|
SELECT
|
||||||
|
ss.conname::name AS constraint_name,
|
||||||
|
ss.nr_nspname::name AS table_schema,
|
||||||
|
ss.relname::name AS table_name,
|
||||||
|
a.attname::name AS column_name,
|
||||||
|
(ss.x).n::integer AS ordinal_position,
|
||||||
|
CASE
|
||||||
|
WHEN ss.contype = 'f' THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n])
|
||||||
|
ELSE NULL::integer
|
||||||
|
END::integer AS position_in_unique_constraint
|
||||||
|
FROM pg_attribute a
|
||||||
|
JOIN (
|
||||||
|
SELECT r.oid AS roid,
|
||||||
|
r.relname,
|
||||||
|
r.relowner,
|
||||||
|
nc.nspname AS nc_nspname,
|
||||||
|
nr.nspname AS nr_nspname,
|
||||||
|
c.oid AS coid,
|
||||||
|
c.conname,
|
||||||
|
c.contype,
|
||||||
|
c.conindid,
|
||||||
|
c.confkey,
|
||||||
|
information_schema._pg_expandarray(c.conkey) AS x
|
||||||
|
FROM pg_namespace nr
|
||||||
|
JOIN pg_class r
|
||||||
|
ON nr.oid = r.relnamespace
|
||||||
|
JOIN pg_constraint c
|
||||||
|
ON r.oid = c.conrelid
|
||||||
|
JOIN pg_namespace nc
|
||||||
|
ON c.connamespace = nc.oid
|
||||||
|
WHERE
|
||||||
|
c.contype in ('p', 'u')
|
||||||
|
AND r.relkind IN ('r', 'p')
|
||||||
|
AND NOT pg_is_other_temp_schema(nr.oid)
|
||||||
|
) ss ON a.attrelid = ss.roid AND a.attnum = (ss.x).x
|
||||||
|
WHERE
|
||||||
|
NOT a.attisdropped
|
||||||
|
),
|
||||||
|
tbl_pk_cols AS (
|
||||||
|
SELECT
|
||||||
|
key_col_usage.table_schema,
|
||||||
|
key_col_usage.table_name,
|
||||||
|
array_agg(key_col_usage.column_name) as pk_cols
|
||||||
|
FROM
|
||||||
|
tbl_constraints
|
||||||
|
JOIN
|
||||||
|
key_col_usage
|
||||||
|
ON
|
||||||
|
key_col_usage.table_name = tbl_constraints.table_name AND
|
||||||
|
key_col_usage.table_schema = tbl_constraints.table_schema AND
|
||||||
|
key_col_usage.constraint_name = tbl_constraints.constraint_name
|
||||||
|
WHERE
|
||||||
|
key_col_usage.table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||||
|
GROUP BY key_col_usage.table_schema, key_col_usage.table_name
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
n.nspname AS table_schema,
|
n.nspname AS table_schema,
|
||||||
c.relname AS table_name,
|
c.relname AS table_name,
|
||||||
@@ -461,10 +518,12 @@ tablesSqlQuery getAll pgVer = [q|
|
|||||||
-- CMD_DELETE
|
-- CMD_DELETE
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 16) = 16
|
AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 16) = 16
|
||||||
)
|
)
|
||||||
) AS deletable
|
) AS deletable,
|
||||||
|
coalesce(tpks.pk_cols, '{}') as pk_cols
|
||||||
FROM pg_class c
|
FROM pg_class c
|
||||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
LEFT JOIN pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
LEFT JOIN pg_description d on d.objoid = c.oid and d.objsubid = 0
|
||||||
|
LEFT JOIN tbl_pk_cols tpks ON n.nspname = tpks.table_schema AND c.relname = tpks.table_name
|
||||||
WHERE c.relkind IN ('v','r','m','f','p')
|
WHERE c.relkind IN ('v','r','m','f','p')
|
||||||
AND n.nspname NOT IN ('pg_catalog', 'information_schema') |] <>
|
AND n.nspname NOT IN ('pg_catalog', 'information_schema') |] <>
|
||||||
relIsPartition <>
|
relIsPartition <>
|
||||||
@@ -613,23 +672,20 @@ columnFromRow tabs (s, t, n, desc, nul, typ, l, d, e) = buildColumn <$> table
|
|||||||
parseEnum :: Maybe Text -> [Text]
|
parseEnum :: Maybe Text -> [Text]
|
||||||
parseEnum = maybe [] (split (==','))
|
parseEnum = maybe [] (split (==','))
|
||||||
|
|
||||||
allM2ORels :: PgVersion -> [Column] -> Bool -> SQL.Statement () [Relationship]
|
allM2ORels :: PgVersion -> Bool -> SQL.Statement () [Relationship]
|
||||||
allM2ORels pgVer allCols =
|
allM2ORels pgVer =
|
||||||
SQL.Statement sql HE.noParams (decodeRels allCols)
|
SQL.Statement sql HE.noParams decodeRels
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
SELECT ns1.nspname AS table_schema,
|
SELECT ns1.nspname AS table_schema,
|
||||||
tab.relname AS table_name,
|
tab.relname AS table_name,
|
||||||
conname AS constraint_name,
|
|
||||||
column_info.cols AS columns,
|
|
||||||
ns2.nspname AS foreign_table_schema,
|
ns2.nspname AS foreign_table_schema,
|
||||||
other.relname AS foreign_table_name,
|
other.relname AS foreign_table_name,
|
||||||
column_info.refs AS foreign_columns
|
conname AS constraint_name,
|
||||||
|
column_info.cols AS columns
|
||||||
FROM pg_constraint,
|
FROM pg_constraint,
|
||||||
LATERAL (
|
LATERAL (
|
||||||
SELECT array_agg(cols.attname) AS cols,
|
SELECT array_agg(row(cols.attname, refs.attname) order by cols.attnum) AS cols
|
||||||
array_agg(cols.attnum) AS nums,
|
|
||||||
array_agg(refs.attname) AS refs
|
|
||||||
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
|
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
|
||||||
LATERAL (SELECT * FROM pg_attribute WHERE attrelid = conrelid AND attnum = col) AS cols,
|
LATERAL (SELECT * FROM pg_attribute WHERE attrelid = conrelid AND attnum = col) AS cols,
|
||||||
LATERAL (SELECT * FROM pg_attribute WHERE attrelid = confrelid AND attnum = ref) AS refs) AS column_info,
|
LATERAL (SELECT * FROM pg_attribute WHERE attrelid = confrelid AND attnum = ref) AS refs) AS column_info,
|
||||||
@@ -641,100 +697,12 @@ allM2ORels pgVer allCols =
|
|||||||
(if pgVer >= pgVersion110
|
(if pgVer >= pgVersion110
|
||||||
then " and conparentid = 0 "
|
then " and conparentid = 0 "
|
||||||
else mempty) <>
|
else mempty) <>
|
||||||
"ORDER BY (conrelid, column_info.nums)"
|
"ORDER BY conrelid, conname"
|
||||||
|
|
||||||
relFromRow :: [Column] -> (Text, Text, Text, [Text], Text, Text, [Text]) -> Maybe Relationship
|
-- | Returns all the views' primary keys and foreign keys dependencies
|
||||||
relFromRow allCols (rs, rt, cn, rcs, frs, frt, frcs) =
|
allViewsKeyDependencies :: Bool -> SQL.Statement ([Schema], [Schema]) [ViewKeyDependency]
|
||||||
Relationship (QualifiedIdentifier rs rt) <$> cols <*> pure (QualifiedIdentifier frs frt) <*> colsF <*> pure (M2O cn)
|
allViewsKeyDependencies =
|
||||||
where
|
SQL.Statement sql (contrazip2 (arrayParam HE.text) (arrayParam HE.text)) decodeViewKeyDeps
|
||||||
findCol s t c = find (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col == c) allCols
|
|
||||||
cols = mapM (findCol rs rt) rcs
|
|
||||||
colsF = mapM (findCol frs frt) frcs
|
|
||||||
|
|
||||||
allPrimaryKeys :: TablesMap -> Bool -> SQL.Statement () [PrimaryKey]
|
|
||||||
allPrimaryKeys tabs =
|
|
||||||
SQL.Statement sql HE.noParams (decodePks tabs)
|
|
||||||
where
|
|
||||||
-- these CTEs are based on the standard "information_schema.table_constraints" and "information_schema.key_column_usage" views,
|
|
||||||
-- we cannot use those directly as they include the following privilege filter:
|
|
||||||
-- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text));
|
|
||||||
sql = [q|
|
|
||||||
WITH tbl_constraints AS (
|
|
||||||
SELECT
|
|
||||||
c.conname::name AS constraint_name,
|
|
||||||
nr.nspname::name AS table_schema,
|
|
||||||
r.relname::name AS table_name
|
|
||||||
FROM pg_namespace nc
|
|
||||||
JOIN pg_constraint c ON nc.oid = c.connamespace
|
|
||||||
JOIN pg_class r ON c.conrelid = r.oid
|
|
||||||
JOIN pg_namespace nr ON nr.oid = r.relnamespace
|
|
||||||
WHERE
|
|
||||||
r.relkind IN ('r', 'p')
|
|
||||||
AND NOT pg_is_other_temp_schema(nr.oid)
|
|
||||||
AND c.contype = 'p'
|
|
||||||
),
|
|
||||||
key_col_usage AS (
|
|
||||||
SELECT
|
|
||||||
ss.conname::name AS constraint_name,
|
|
||||||
ss.nr_nspname::name AS table_schema,
|
|
||||||
ss.relname::name AS table_name,
|
|
||||||
a.attname::name AS column_name,
|
|
||||||
(ss.x).n::integer AS ordinal_position,
|
|
||||||
CASE
|
|
||||||
WHEN ss.contype = 'f' THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n])
|
|
||||||
ELSE NULL::integer
|
|
||||||
END::integer AS position_in_unique_constraint
|
|
||||||
FROM pg_attribute a
|
|
||||||
JOIN (
|
|
||||||
SELECT r.oid AS roid,
|
|
||||||
r.relname,
|
|
||||||
r.relowner,
|
|
||||||
nc.nspname AS nc_nspname,
|
|
||||||
nr.nspname AS nr_nspname,
|
|
||||||
c.oid AS coid,
|
|
||||||
c.conname,
|
|
||||||
c.contype,
|
|
||||||
c.conindid,
|
|
||||||
c.confkey,
|
|
||||||
information_schema._pg_expandarray(c.conkey) AS x
|
|
||||||
FROM pg_namespace nr
|
|
||||||
JOIN pg_class r
|
|
||||||
ON nr.oid = r.relnamespace
|
|
||||||
JOIN pg_constraint c
|
|
||||||
ON r.oid = c.conrelid
|
|
||||||
JOIN pg_namespace nc
|
|
||||||
ON c.connamespace = nc.oid
|
|
||||||
WHERE
|
|
||||||
c.contype in ('p', 'u')
|
|
||||||
AND r.relkind IN ('r', 'p')
|
|
||||||
AND NOT pg_is_other_temp_schema(nr.oid)
|
|
||||||
) ss ON a.attrelid = ss.roid AND a.attnum = (ss.x).x
|
|
||||||
WHERE
|
|
||||||
NOT a.attisdropped
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
key_col_usage.table_schema,
|
|
||||||
key_col_usage.table_name,
|
|
||||||
key_col_usage.column_name
|
|
||||||
FROM
|
|
||||||
tbl_constraints
|
|
||||||
JOIN
|
|
||||||
key_col_usage
|
|
||||||
ON
|
|
||||||
key_col_usage.table_name = tbl_constraints.table_name AND
|
|
||||||
key_col_usage.table_schema = tbl_constraints.table_schema AND
|
|
||||||
key_col_usage.constraint_name = tbl_constraints.constraint_name
|
|
||||||
WHERE
|
|
||||||
key_col_usage.table_schema NOT IN ('pg_catalog', 'information_schema') |]
|
|
||||||
|
|
||||||
pkFromRow :: TablesMap -> (Schema, Text, Text) -> Maybe PrimaryKey
|
|
||||||
pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n
|
|
||||||
where table = M.lookup (QualifiedIdentifier s t) tabs
|
|
||||||
|
|
||||||
-- returns all the primary and foreign key columns which are referenced in views
|
|
||||||
pfkSourceColumns :: [Column] -> Bool -> SQL.Statement ([Schema], [Schema]) [SourceColumn]
|
|
||||||
pfkSourceColumns cols =
|
|
||||||
SQL.Statement sql (contrazip2 (arrayParam HE.text) (arrayParam HE.text)) (decodeSourceColumns cols)
|
|
||||||
-- query explanation at:
|
-- query explanation at:
|
||||||
-- * rationale: https://gist.github.com/wolfgangwalther/5425d64e7b0d20aad71f6f68474d9f19
|
-- * rationale: https://gist.github.com/wolfgangwalther/5425d64e7b0d20aad71f6f68474d9f19
|
||||||
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
|
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
|
||||||
@@ -744,6 +712,8 @@ pfkSourceColumns cols =
|
|||||||
pks_fks as (
|
pks_fks as (
|
||||||
-- pk + fk referencing col
|
-- pk + fk referencing col
|
||||||
select
|
select
|
||||||
|
contype,
|
||||||
|
conname,
|
||||||
conrelid as resorigtbl,
|
conrelid as resorigtbl,
|
||||||
unnest(conkey) as resorigcol
|
unnest(conkey) as resorigcol
|
||||||
from pg_constraint
|
from pg_constraint
|
||||||
@@ -751,6 +721,8 @@ pfkSourceColumns cols =
|
|||||||
union
|
union
|
||||||
-- fk referenced col
|
-- fk referenced col
|
||||||
select
|
select
|
||||||
|
concat(contype, '_ref') as contype,
|
||||||
|
conname,
|
||||||
confrelid,
|
confrelid,
|
||||||
unnest(confkey)
|
unnest(confkey)
|
||||||
from pg_constraint
|
from pg_constraint
|
||||||
@@ -879,17 +851,19 @@ pfkSourceColumns cols =
|
|||||||
select
|
select
|
||||||
sch.nspname as table_schema,
|
sch.nspname as table_schema,
|
||||||
tbl.relname as table_name,
|
tbl.relname as table_name,
|
||||||
col.attname as table_column_name,
|
|
||||||
rec.view_schema,
|
rec.view_schema,
|
||||||
rec.view_name,
|
rec.view_name,
|
||||||
vcol.attname as view_column_name
|
pks_fks.conname as constraint_name,
|
||||||
|
pks_fks.contype as constraint_type,
|
||||||
|
array_agg(row(col.attname, vcol.attname) order by col.attnum) as column_dependencies
|
||||||
from recursion rec
|
from recursion rec
|
||||||
join pg_class tbl on tbl.oid = rec.resorigtbl
|
join pg_class tbl on tbl.oid = rec.resorigtbl
|
||||||
join pg_attribute col on col.attrelid = tbl.oid and col.attnum = rec.resorigcol
|
join pg_attribute col on col.attrelid = tbl.oid and col.attnum = rec.resorigcol
|
||||||
join pg_attribute vcol on vcol.attrelid = rec.view_id and vcol.attnum = rec.view_column
|
join pg_attribute vcol on vcol.attrelid = rec.view_id and vcol.attnum = rec.view_column
|
||||||
join pg_namespace sch on sch.oid = tbl.relnamespace
|
join pg_namespace sch on sch.oid = tbl.relnamespace
|
||||||
join pks_fks using (resorigtbl, resorigcol)
|
join pks_fks using (resorigtbl, resorigcol)
|
||||||
order by view_schema, view_name, view_column_name; |]
|
group by sch.nspname, tbl.relname, rec.view_schema, rec.view_name, pks_fks.conname, pks_fks.contype
|
||||||
|
|]
|
||||||
|
|
||||||
param :: HE.Value a -> HE.Params a
|
param :: HE.Value a -> HE.Params a
|
||||||
param = HE.param . HE.nonNullable
|
param = HE.param . HE.nonNullable
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
module PostgREST.DbStructure.Relationship
|
module PostgREST.DbStructure.Relationship
|
||||||
( Cardinality(..)
|
( Cardinality(..)
|
||||||
, PrimaryKey(..)
|
|
||||||
, Relationship(..)
|
, Relationship(..)
|
||||||
, Junction(..)
|
, Junction(..)
|
||||||
, isSelfReference
|
, isSelfReference
|
||||||
@@ -11,24 +10,17 @@ module PostgREST.DbStructure.Relationship
|
|||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier)
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
import PostgREST.DbStructure.Table (Column (..), Table)
|
QualifiedIdentifier)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
-- | Relationship between two tables.
|
-- | Relationship between two tables.
|
||||||
--
|
|
||||||
-- The order of the relColumns and relForeignColumns should be maintained to get the
|
|
||||||
-- join conditions right.
|
|
||||||
--
|
|
||||||
-- TODO merge relColumns and relForeignColumns to a tuple or Data.Bimap
|
|
||||||
data Relationship = Relationship
|
data Relationship = Relationship
|
||||||
{ relTable :: QualifiedIdentifier
|
{ relTable :: QualifiedIdentifier
|
||||||
, relColumns :: [Column]
|
, relForeignTable :: QualifiedIdentifier
|
||||||
, relForeignTable :: QualifiedIdentifier
|
, relCardinality :: Cardinality
|
||||||
, relForeignColumns :: [Column]
|
|
||||||
, relCardinality :: Cardinality
|
|
||||||
}
|
}
|
||||||
deriving (Eq, Generic, JSON.ToJSON)
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
@@ -36,9 +28,12 @@ data Relationship = Relationship
|
|||||||
-- | https://en.wikipedia.org/wiki/Cardinality_(data_modeling)
|
-- | https://en.wikipedia.org/wiki/Cardinality_(data_modeling)
|
||||||
-- TODO: missing one-to-one
|
-- TODO: missing one-to-one
|
||||||
data Cardinality
|
data Cardinality
|
||||||
= O2M FKConstraint -- ^ one-to-many cardinality
|
= O2M {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}
|
||||||
| M2O FKConstraint -- ^ many-to-one cardinality
|
-- ^ one-to-many
|
||||||
| M2M Junction -- ^ many-to-many cardinality
|
| M2O {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}
|
||||||
|
-- ^ many-to-one
|
||||||
|
| M2M Junction
|
||||||
|
-- ^ many-to-many
|
||||||
deriving (Eq, Generic, JSON.ToJSON)
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
type FKConstraint = Text
|
type FKConstraint = Text
|
||||||
@@ -47,17 +42,11 @@ type FKConstraint = Text
|
|||||||
data Junction = Junction
|
data Junction = Junction
|
||||||
{ junTable :: QualifiedIdentifier
|
{ junTable :: QualifiedIdentifier
|
||||||
, junConstraint1 :: FKConstraint
|
, junConstraint1 :: FKConstraint
|
||||||
, junColumns1 :: [Column]
|
|
||||||
, junConstraint2 :: FKConstraint
|
, junConstraint2 :: FKConstraint
|
||||||
, junColumns2 :: [Column]
|
, junColumns1 :: [(FieldName, FieldName)]
|
||||||
|
, junColumns2 :: [(FieldName, FieldName)]
|
||||||
}
|
}
|
||||||
deriving (Eq, Generic, JSON.ToJSON)
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
isSelfReference :: Relationship -> Bool
|
isSelfReference :: Relationship -> Bool
|
||||||
isSelfReference r = relTable r == relForeignTable r
|
isSelfReference r = relTable r == relForeignTable r
|
||||||
|
|
||||||
data PrimaryKey = PrimaryKey
|
|
||||||
{ pkTable :: Table
|
|
||||||
, pkName :: Text
|
|
||||||
}
|
|
||||||
deriving (Generic, JSON.ToJSON)
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ data Table = Table
|
|||||||
, tableInsertable :: Bool
|
, tableInsertable :: Bool
|
||||||
, tableUpdatable :: Bool
|
, tableUpdatable :: Bool
|
||||||
, tableDeletable :: Bool
|
, tableDeletable :: Bool
|
||||||
|
, tablePKCols :: [FieldName]
|
||||||
}
|
}
|
||||||
deriving (Show, Ord, Generic, JSON.ToJSON)
|
deriving (Show, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ import PostgREST.DbStructure.Proc (ProcDescription (..),
|
|||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..))
|
Relationship (..))
|
||||||
import PostgREST.DbStructure.Table (Column (..))
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
@@ -158,15 +156,15 @@ compressedRel Relationship{..} =
|
|||||||
: case relCardinality of
|
: case relCardinality of
|
||||||
M2M Junction{..} -> [
|
M2M Junction{..} -> [
|
||||||
"cardinality" .= ("many-to-many" :: Text)
|
"cardinality" .= ("many-to-many" :: Text)
|
||||||
, "relationship" .= (qiName junTable <> " using " <> junConstraint1 <> fmtEls (colName <$> junColumns1) <> " and " <> junConstraint2 <> fmtEls (colName <$> junColumns2))
|
, "relationship" .= (qiName junTable <> " using " <> junConstraint1 <> fmtEls (snd <$> junColumns1) <> " and " <> junConstraint2 <> fmtEls (snd <$> junColumns2))
|
||||||
]
|
]
|
||||||
M2O cons -> [
|
M2O cons relColumns -> [
|
||||||
"cardinality" .= ("many-to-one" :: Text)
|
"cardinality" .= ("many-to-one" :: Text)
|
||||||
, "relationship" .= (cons <> " using " <> qiName relTable <> fmtEls (colName <$> relColumns) <> " and " <> qiName relForeignTable <> fmtEls (colName <$> relForeignColumns))
|
, "relationship" .= (cons <> " using " <> qiName relTable <> fmtEls (fst <$> relColumns) <> " and " <> qiName relForeignTable <> fmtEls (snd <$> relColumns))
|
||||||
]
|
]
|
||||||
O2M cons -> [
|
O2M cons relColumns -> [
|
||||||
"cardinality" .= ("one-to-many" :: Text)
|
"cardinality" .= ("one-to-many" :: Text)
|
||||||
, "relationship" .= (cons <> " using " <> qiName relTable <> fmtEls (colName <$> relColumns) <> " and " <> qiName relForeignTable <> fmtEls (colName <$> relForeignColumns))
|
, "relationship" .= (cons <> " using " <> qiName relTable <> fmtEls (fst <$> relColumns) <> " and " <> qiName relForeignTable <> fmtEls (snd <$> relColumns))
|
||||||
]
|
]
|
||||||
|
|
||||||
relHint :: [Relationship] -> Text
|
relHint :: [Relationship] -> Text
|
||||||
@@ -176,8 +174,8 @@ relHint rels = T.intercalate ", " (hintList <$> rels)
|
|||||||
let buildHint rel = "'" <> qiName relForeignTable <> "!" <> rel <> "'" in
|
let buildHint rel = "'" <> qiName relForeignTable <> "!" <> rel <> "'" in
|
||||||
case relCardinality of
|
case relCardinality of
|
||||||
M2M Junction{..} -> buildHint (qiName junTable)
|
M2M Junction{..} -> buildHint (qiName junTable)
|
||||||
M2O cons -> buildHint cons
|
M2O cons _ -> buildHint cons
|
||||||
O2M cons -> buildHint cons
|
O2M cons _ -> buildHint cons
|
||||||
|
|
||||||
data PgError = PgError Authenticated SQL.UsageError
|
data PgError = PgError Authenticated SQL.UsageError
|
||||||
type Authenticated = Bool
|
type Authenticated = Bool
|
||||||
|
|||||||
+26
-30
@@ -3,7 +3,6 @@ Module : PostgREST.OpenAPI
|
|||||||
Description : Generates the OpenAPI output
|
Description : Generates the OpenAPI output
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
module PostgREST.OpenAPI (encode) where
|
module PostgREST.OpenAPI (encode) where
|
||||||
|
|
||||||
@@ -28,12 +27,11 @@ import Data.Swagger
|
|||||||
import PostgREST.Config (AppConfig (..), Proxy (..),
|
import PostgREST.Config (AppConfig (..), Proxy (..),
|
||||||
isMalformedProxyUri, toURI)
|
isMalformedProxyUri, toURI)
|
||||||
import PostgREST.DbStructure (DbStructure (..),
|
import PostgREST.DbStructure (DbStructure (..),
|
||||||
tableCols, tablePKCols)
|
tableCols)
|
||||||
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
|
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
|
||||||
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||||
ProcParam (..))
|
ProcParam (..))
|
||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
PrimaryKey (..),
|
|
||||||
Relationship (..))
|
Relationship (..))
|
||||||
import PostgREST.DbStructure.Table (Column (..), Table (..),
|
import PostgREST.DbStructure.Table (Column (..), Table (..),
|
||||||
TablesMap)
|
TablesMap)
|
||||||
@@ -52,7 +50,6 @@ encode conf dbStructure tables procs schemaDescription =
|
|||||||
(openApiTableInfo dbStructure <$> (snd <$> M.toList tables))
|
(openApiTableInfo dbStructure <$> (snd <$> M.toList tables))
|
||||||
(proxyUri conf)
|
(proxyUri conf)
|
||||||
schemaDescription
|
schemaDescription
|
||||||
(dbPrimaryKeys dbStructure)
|
|
||||||
|
|
||||||
makeMimeList :: [ContentType] -> MimeList
|
makeMimeList :: [ContentType] -> MimeList
|
||||||
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
|
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
|
||||||
@@ -83,34 +80,34 @@ parseDefault colType colDefault =
|
|||||||
where
|
where
|
||||||
wrapInQuotations text = "\"" <> text <> "\""
|
wrapInQuotations text = "\"" <> text <> "\""
|
||||||
|
|
||||||
makeTableDef :: [Relationship] -> [PrimaryKey] -> (Table, [Column], [Text]) -> (Text, Schema)
|
makeTableDef :: [Relationship] -> (Table, [Column]) -> (Text, Schema)
|
||||||
makeTableDef rels pks (t, cs, _) =
|
makeTableDef rels (t, cs) =
|
||||||
let tn = tableName t in
|
let tn = tableName t in
|
||||||
(tn, (mempty :: Schema)
|
(tn, (mempty :: Schema)
|
||||||
& description .~ tableDescription t
|
& description .~ tableDescription t
|
||||||
& type_ ?~ SwaggerObject
|
& type_ ?~ SwaggerObject
|
||||||
& properties .~ fromList (fmap (makeProperty rels pks) cs)
|
& properties .~ fromList (fmap (makeProperty rels) cs)
|
||||||
& required .~ fmap colName (filter (not . colNullable) cs))
|
& required .~ fmap colName (filter (not . colNullable) cs))
|
||||||
|
|
||||||
makeProperty :: [Relationship] -> [PrimaryKey] -> Column -> (Text, Referenced Schema)
|
makeProperty :: [Relationship] -> Column -> (Text, Referenced Schema)
|
||||||
makeProperty rels pks c = (colName c, Inline s)
|
makeProperty rels col = (colName col, Inline s)
|
||||||
where
|
where
|
||||||
e = if null $ colEnum c then Nothing else JSON.decode $ JSON.encode $ colEnum c
|
e = if null $ colEnum col then Nothing else JSON.decode $ JSON.encode $ colEnum col
|
||||||
fk :: Maybe Text
|
fk :: Maybe Text
|
||||||
fk =
|
fk =
|
||||||
let
|
let
|
||||||
-- Finds the relationship that has a single column foreign key
|
-- Finds the relationship that has a single column foreign key
|
||||||
rel = find (\case
|
rel = find (\case
|
||||||
Relationship{relColumns, relCardinality=M2O _} -> [c] == relColumns
|
Relationship{relCardinality=(M2O _ relColumns)} -> [colName col] == (fst <$> relColumns)
|
||||||
_ -> False
|
_ -> False
|
||||||
) rels
|
) rels
|
||||||
fCol = colName <$> (headMay . relForeignColumns =<< rel)
|
fCol = (headMay . (\r -> snd <$> relColumns (relCardinality r)) =<< rel)
|
||||||
fTbl = qiName . relForeignTable <$> rel
|
fTbl = qiName . relForeignTable <$> rel
|
||||||
fTblCol = (,) <$> fTbl <*> fCol
|
fTblCol = (,) <$> fTbl <*> fCol
|
||||||
in
|
in
|
||||||
(\(a, b) -> T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]) <$> fTblCol
|
(\(a, b) -> T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]) <$> fTblCol
|
||||||
pk :: Bool
|
pk :: Bool
|
||||||
pk = any (\p -> pkTable p == colTable c && pkName p == colName c) pks
|
pk = colName col `elem` tablePKCols (colTable col)
|
||||||
n = catMaybes
|
n = catMaybes
|
||||||
[ Just "Note:"
|
[ Just "Note:"
|
||||||
, if pk then Just "This is a Primary Key.<pk/>" else Nothing
|
, if pk then Just "This is a Primary Key.<pk/>" else Nothing
|
||||||
@@ -118,17 +115,17 @@ makeProperty rels pks c = (colName c, Inline s)
|
|||||||
]
|
]
|
||||||
d =
|
d =
|
||||||
if length n > 1 then
|
if length n > 1 then
|
||||||
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription c) (T.intercalate "\n" n)
|
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
||||||
else
|
else
|
||||||
colDescription c
|
colDescription col
|
||||||
s =
|
s =
|
||||||
(mempty :: Schema)
|
(mempty :: Schema)
|
||||||
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType c) =<< colDefault c)
|
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
|
||||||
& description .~ d
|
& description .~ d
|
||||||
& enum_ .~ e
|
& enum_ .~ e
|
||||||
& format ?~ colType c
|
& format ?~ colType col
|
||||||
& maxLength .~ (fromIntegral <$> colMaxLen c)
|
& maxLength .~ (fromIntegral <$> colMaxLen col)
|
||||||
& type_ .~ toSwaggerType (colType c)
|
& type_ .~ toSwaggerType (colType col)
|
||||||
|
|
||||||
makeProcSchema :: ProcDescription -> Schema
|
makeProcSchema :: ProcDescription -> Schema
|
||||||
makeProcSchema pd =
|
makeProcSchema pd =
|
||||||
@@ -165,7 +162,7 @@ makeProcParam pd =
|
|||||||
, Ref $ Reference "preferParams"
|
, Ref $ Reference "preferParams"
|
||||||
]
|
]
|
||||||
|
|
||||||
makeParamDefs :: [(Table, [Column], [Text])] -> [(Text, Param)]
|
makeParamDefs :: [(Table, [Column])] -> [(Text, Param)]
|
||||||
makeParamDefs ti =
|
makeParamDefs ti =
|
||||||
[ ("preferParams", makePreferParam ["params=single-object"])
|
[ ("preferParams", makePreferParam ["params=single-object"])
|
||||||
, ("preferReturn", makePreferParam ["return=representation", "return=minimal", "return=none"])
|
, ("preferReturn", makePreferParam ["return=representation", "return=minimal", "return=none"])
|
||||||
@@ -222,7 +219,7 @@ makeParamDefs ti =
|
|||||||
& type_ ?~ SwaggerString))
|
& type_ ?~ SwaggerString))
|
||||||
]
|
]
|
||||||
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) cs
|
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) cs
|
||||||
| (t, cs, _) <- ti
|
| (t, cs) <- ti
|
||||||
]
|
]
|
||||||
|
|
||||||
makeObjectBody :: Text -> (Text, Param)
|
makeObjectBody :: Text -> (Text, Param)
|
||||||
@@ -247,8 +244,8 @@ makeRowFilter tn c =
|
|||||||
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
|
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
|
||||||
makeRowFilters tn = fmap (makeRowFilter tn)
|
makeRowFilters tn = fmap (makeRowFilter tn)
|
||||||
|
|
||||||
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
makePathItem :: (Table, [Column]) -> (FilePath, PathItem)
|
||||||
makePathItem (t, cs, _) = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t)
|
makePathItem (t, cs) = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t)
|
||||||
where
|
where
|
||||||
-- Use first line of table description as summary; rest as description (if present)
|
-- Use first line of table description as summary; rest as description (if present)
|
||||||
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
||||||
@@ -312,7 +309,7 @@ makeRootPathItem = ("/", p)
|
|||||||
pr = (mempty :: PathItem) & get ?~ getOp
|
pr = (mempty :: PathItem) & get ?~ getOp
|
||||||
p = pr
|
p = pr
|
||||||
|
|
||||||
makePathItems :: [ProcDescription] -> [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
|
makePathItems :: [ProcDescription] -> [(Table, [Column])] -> InsOrdHashMap FilePath PathItem
|
||||||
makePathItems pds ti = fromList $ makeRootPathItem :
|
makePathItems pds ti = fromList $ makeRootPathItem :
|
||||||
fmap makePathItem ti ++ fmap makeProcPathItem pds
|
fmap makePathItem ti ++ fmap makeProcPathItem pds
|
||||||
|
|
||||||
@@ -324,8 +321,8 @@ escapeHostName "*6" = "0.0.0.0"
|
|||||||
escapeHostName "!6" = "0.0.0.0"
|
escapeHostName "!6" = "0.0.0.0"
|
||||||
escapeHostName h = h
|
escapeHostName h = h
|
||||||
|
|
||||||
postgrestSpec :: [Relationship] -> [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> Swagger
|
postgrestSpec :: [Relationship] -> [ProcDescription] -> [(Table, [Column])] -> (Text, Text, Integer, Text) -> Maybe Text -> Swagger
|
||||||
postgrestSpec rels pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
|
postgrestSpec rels pds ti (s, h, p, b) sd = (mempty :: Swagger)
|
||||||
& basePath ?~ T.unpack b
|
& basePath ?~ T.unpack b
|
||||||
& schemes ?~ [s']
|
& schemes ?~ [s']
|
||||||
& info .~ ((mempty :: Info)
|
& info .~ ((mempty :: Info)
|
||||||
@@ -336,7 +333,7 @@ postgrestSpec rels pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
|
|||||||
& description ?~ "PostgREST Documentation"
|
& description ?~ "PostgREST Documentation"
|
||||||
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
||||||
& host .~ h'
|
& host .~ h'
|
||||||
& definitions .~ fromList (makeTableDef rels pks <$> ti)
|
& definitions .~ fromList (makeTableDef rels <$> ti)
|
||||||
& parameters .~ fromList (makeParamDefs ti)
|
& parameters .~ fromList (makeParamDefs ti)
|
||||||
& paths .~ makePathItems pds ti
|
& paths .~ makePathItems pds ti
|
||||||
& produces .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
& produces .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
@@ -383,9 +380,8 @@ proxyUri AppConfig{..} =
|
|||||||
Nothing ->
|
Nothing ->
|
||||||
("http", configServerHost, toInteger configServerPort, "/")
|
("http", configServerHost, toInteger configServerPort, "/")
|
||||||
|
|
||||||
openApiTableInfo :: DbStructure -> Table -> (Table, [Column], [Text])
|
openApiTableInfo :: DbStructure -> Table -> (Table, [Column])
|
||||||
openApiTableInfo dbStructure table =
|
openApiTableInfo dbStructure table =
|
||||||
( table
|
( table
|
||||||
, tableCols dbStructure (tableSchema table) (tableName table)
|
, tableCols dbStructure (tableSchema table) (tableName table)
|
||||||
, tablePKCols dbStructure (tableSchema table) (tableName table)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ getJoinsSelects :: ReadRequest -> ([SQL.Snippet], [SQL.Snippet]) -> ([SQL.Snippe
|
|||||||
getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTable=QualifiedIdentifier{qiName=table}}, alias, _, joinType, _)) _) (joins,selects) =
|
getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTable=QualifiedIdentifier{qiName=table}}, alias, _, joinType, _)) _) (joins,selects) =
|
||||||
let subquery = readRequestToQuery rr in
|
let subquery = readRequestToQuery rr in
|
||||||
case card of
|
case card of
|
||||||
M2O _ ->
|
M2O _ _ ->
|
||||||
let aliasOrName = fromMaybe name alias
|
let aliasOrName = fromMaybe name alias
|
||||||
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
|
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
|
||||||
sel = SQL.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName)
|
sel = SQL.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName)
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import PostgREST.DbStructure.Proc (ProcDescription (..),
|
|||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..))
|
Relationship (..))
|
||||||
import PostgREST.DbStructure.Table (Column (..))
|
|
||||||
import PostgREST.Error (Error (..))
|
import PostgREST.Error (Error (..))
|
||||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
@@ -163,16 +162,23 @@ findRel schema allRels origin target hint =
|
|||||||
-- cardinalities(m2o/o2m) We output the O2M rel, the M2O rel can be
|
-- cardinalities(m2o/o2m) We output the O2M rel, the M2O rel can be
|
||||||
-- obtained by using the origin column as an embed hint.
|
-- obtained by using the origin column as an embed hint.
|
||||||
rs@[rel0, rel1] -> case (relCardinality rel0, relCardinality rel1, relTable rel0 == relTable rel1 && relForeignTable rel0 == relForeignTable rel1) of
|
rs@[rel0, rel1] -> case (relCardinality rel0, relCardinality rel1, relTable rel0 == relTable rel1 && relForeignTable rel0 == relForeignTable rel1) of
|
||||||
(O2M cons1, M2O cons2, True) -> if cons1 == cons2 then Right rel0 else Left $ AmbiguousRelBetween origin target rs
|
(O2M cons1 _, M2O cons2 _, True) -> if cons1 == cons2 then Right rel0 else Left $ AmbiguousRelBetween origin target rs
|
||||||
(M2O cons1, O2M cons2, True) -> if cons1 == cons2 then Right rel1 else Left $ AmbiguousRelBetween origin target rs
|
(M2O cons1 _, O2M cons2 _, True) -> if cons1 == cons2 then Right rel1 else Left $ AmbiguousRelBetween origin target rs
|
||||||
_ -> Left $ AmbiguousRelBetween origin target rs
|
_ -> Left $ AmbiguousRelBetween origin target rs
|
||||||
rs -> Left $ AmbiguousRelBetween origin target rs
|
rs -> Left $ AmbiguousRelBetween origin target rs
|
||||||
where
|
where
|
||||||
matchFKSingleCol hint_ cols = length cols == 1 && hint_ == (colName <$> head cols)
|
matchFKSingleCol hint_ card = case card of
|
||||||
|
O2M _ cols -> length cols == 1 && hint_ == head (fst <$> cols)
|
||||||
|
M2O _ cols -> length cols == 1 && hint_ == head (fst <$> cols)
|
||||||
|
_ -> False
|
||||||
|
matchFKRefSingleCol hint_ card = case card of
|
||||||
|
O2M _ cols -> length cols == 1 && hint_ == head (snd <$> cols)
|
||||||
|
M2O _ cols -> length cols == 1 && hint_ == head (snd <$> cols)
|
||||||
|
_ -> False
|
||||||
matchConstraint tar card = case card of
|
matchConstraint tar card = case card of
|
||||||
O2M cons -> tar == Just cons
|
O2M cons _ -> tar == Just cons
|
||||||
M2O cons -> tar == Just cons
|
M2O cons _ -> tar == Just cons
|
||||||
_ -> False
|
_ -> False
|
||||||
matchJunction hint_ card = case card of
|
matchJunction hint_ card = case card of
|
||||||
M2M Junction{junTable} -> hint_ == Just (qiName junTable)
|
M2M Junction{junTable} -> hint_ == Just (qiName junTable)
|
||||||
_ -> False
|
_ -> False
|
||||||
@@ -192,8 +198,8 @@ findRel schema allRels origin target hint =
|
|||||||
) ||
|
) ||
|
||||||
-- /projects?select=client_id(*)
|
-- /projects?select=client_id(*)
|
||||||
(
|
(
|
||||||
origin == qiName relTable && -- projects
|
origin == qiName relTable && -- projects
|
||||||
matchFKSingleCol (Just target) relColumns -- client_id
|
matchFKSingleCol (Just target) relCardinality -- client_id
|
||||||
)
|
)
|
||||||
) && (
|
) && (
|
||||||
isNothing hint || -- hint is optional
|
isNothing hint || -- hint is optional
|
||||||
@@ -202,8 +208,8 @@ findRel schema allRels origin target hint =
|
|||||||
matchConstraint hint relCardinality || -- projects_client_id_fkey
|
matchConstraint hint relCardinality || -- projects_client_id_fkey
|
||||||
|
|
||||||
-- /projects?select=clients!client_id(*) or /projects?select=clients!id(*)
|
-- /projects?select=clients!client_id(*) or /projects?select=clients!id(*)
|
||||||
matchFKSingleCol hint relColumns || -- client_id
|
matchFKSingleCol hint relCardinality || -- client_id
|
||||||
matchFKSingleCol hint relForeignColumns || -- id
|
matchFKRefSingleCol hint relCardinality || -- id
|
||||||
|
|
||||||
-- /users?select=tasks!users_tasks(*) many-to-many between users and tasks
|
-- /users?select=tasks!users_tasks(*) many-to-many between users and tasks
|
||||||
matchJunction hint relCardinality -- users_tasks
|
matchJunction hint relCardinality -- users_tasks
|
||||||
@@ -234,19 +240,21 @@ addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_
|
|||||||
|
|
||||||
-- previousAlias and newAlias are used in the case of self joins
|
-- previousAlias and newAlias are used in the case of self joins
|
||||||
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relationship -> [JoinCondition]
|
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relationship -> [JoinCondition]
|
||||||
getJoinConditions previousAlias newAlias (Relationship QualifiedIdentifier{qiSchema=tSchema, qiName=tN} cols QualifiedIdentifier{qiName=ftN} fCols card) =
|
getJoinConditions previousAlias newAlias (Relationship QualifiedIdentifier{qiSchema=tSchema, qiName=tN} QualifiedIdentifier{qiName=ftN} card) =
|
||||||
case card of
|
case card of
|
||||||
M2M (Junction QualifiedIdentifier{qiName=jtn} _ jc1 _ jc2) ->
|
M2M (Junction QualifiedIdentifier{qiName=jtn} _ _ jcols1 jcols2) ->
|
||||||
zipWith (toJoinCondition tN jtn) cols jc1 ++ zipWith (toJoinCondition ftN jtn) fCols jc2
|
(toJoinCondition tN jtn <$> jcols1) ++ (toJoinCondition ftN jtn <$> jcols2)
|
||||||
_ ->
|
O2M _ cols ->
|
||||||
zipWith (toJoinCondition tN ftN) cols fCols
|
toJoinCondition tN ftN <$> cols
|
||||||
|
M2O _ cols ->
|
||||||
|
toJoinCondition tN ftN <$> cols
|
||||||
where
|
where
|
||||||
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
|
toJoinCondition :: Text -> Text -> (FieldName, FieldName) -> JoinCondition
|
||||||
toJoinCondition tb ftb c fc =
|
toJoinCondition tb ftb (c, fc) =
|
||||||
let qi1 = removeSourceCTESchema tSchema tb
|
let qi1 = removeSourceCTESchema tSchema tb
|
||||||
qi2 = removeSourceCTESchema tSchema ftb in
|
qi2 = removeSourceCTESchema tSchema ftb in
|
||||||
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, colName c)
|
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, c)
|
||||||
(maybe qi2 (QualifiedIdentifier mempty) newAlias, colName fc)
|
(maybe qi2 (QualifiedIdentifier mempty) newAlias, fc)
|
||||||
|
|
||||||
-- On mutation and calling proc cases we wrap the target table in a WITH
|
-- On mutation and calling proc cases we wrap the target table in a WITH
|
||||||
-- {sourceCTEName} if this happens remove the schema `FROM
|
-- {sourceCTEName} if this happens remove the schema `FROM
|
||||||
@@ -380,7 +388,9 @@ returningCols rr@(Node _ forest) pkCols
|
|||||||
-- projects. So this adds the foreign key columns to ensure the embedding
|
-- projects. So this adds the foreign key columns to ensure the embedding
|
||||||
-- succeeds, result would be `RETURNING name, client_id`.
|
-- succeeds, result would be `RETURNING name, client_id`.
|
||||||
fkCols = concat $ mapMaybe (\case
|
fkCols = concat $ mapMaybe (\case
|
||||||
Node (_, (_, Just Relationship{relColumns=cols}, _, _, _, _)) _ -> Just cols
|
Node (_, (_, Just Relationship{relCardinality=O2M _ cols}, _, _, _, _)) _ -> Just $ fst <$> cols
|
||||||
|
Node (_, (_, Just Relationship{relCardinality=M2O _ cols}, _, _, _, _)) _ -> Just $ fst <$> cols
|
||||||
|
Node (_, (_, Just Relationship{relCardinality=M2M Junction{junColumns1, junColumns2}}, _, _, _, _)) _ -> Just $ (fst <$> junColumns1) ++ (fst <$> junColumns2)
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
) forest
|
) forest
|
||||||
-- However if the "client_id" is present, e.g. mutateRequest to
|
-- However if the "client_id" is present, e.g. mutateRequest to
|
||||||
@@ -390,7 +400,7 @@ returningCols rr@(Node _ forest) pkCols
|
|||||||
-- deduplicate with Set: We are adding the primary key columns as well to
|
-- deduplicate with Set: We are adding the primary key columns as well to
|
||||||
-- make sure, that a proper location header can always be built for
|
-- make sure, that a proper location header can always be built for
|
||||||
-- INSERT/POST
|
-- INSERT/POST
|
||||||
returnings = S.toList . S.fromList $ fldNames ++ (colName <$> fkCols) ++ pkCols
|
returnings = S.toList . S.fromList $ fldNames ++ fkCols ++ pkCols
|
||||||
|
|
||||||
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
|
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
|
||||||
-- they are later concatenated with AND in the QueryBuilder
|
-- they are later concatenated with AND in the QueryBuilder
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ spec =
|
|||||||
request methodGet "/"
|
request methodGet "/"
|
||||||
[("Accept", "application/json")] "" `shouldRespondWith`
|
[("Accept", "application/json")] "" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"qiSchema":"test","qiName":"orders_view"
|
"qiSchema":"test","qiName":"has_fk"
|
||||||
} |]
|
} |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -595,7 +595,7 @@ spec actualPgVersion = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
context "requesting header only representation" $ do
|
context "requesting header only representation" $ do
|
||||||
it "returns a location header" $
|
it "returns a location header with a composite PK col" $
|
||||||
request methodPost "/compound_pk_view" [("Prefer", "return=headers-only")]
|
request methodPost "/compound_pk_view" [("Prefer", "return=headers-only")]
|
||||||
[json|{"k1":1,"k2":"test","extra":2}|]
|
[json|{"k1":1,"k2":"test","extra":2}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
@@ -606,13 +606,13 @@ spec actualPgVersion = do
|
|||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "should not throw and return location header when a PK is null" $
|
it "returns location header with a single PK col" $
|
||||||
request methodPost "/test_null_pk_competitors_sponsors" [("Prefer", "return=headers-only")]
|
request methodPost "/test_null_pk_competitors_sponsors" [("Prefer", "return=headers-only")]
|
||||||
[json|{"id":1}|]
|
[json|{"id":1}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
, "Location" <:> "/test_null_pk_competitors_sponsors?id=eq.1&sponsor_id=is.null"
|
, "Location" <:> "/test_null_pk_competitors_sponsors?id=eq.1"
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user