diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a83abeac..615917361 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
- #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
+ - #2254, Fix inferring a foreign key column as a primary key column on views - @steve-chavez
### Changed
diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs
index c00076507..a7b3174ed 100644
--- a/src/PostgREST/App.hs
+++ b/src/PostgREST/App.hs
@@ -63,7 +63,7 @@ import PostgREST.Config (AppConfig (..),
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.ContentType (ContentType (..))
import PostgREST.DbStructure (DbStructure (..),
- findIfView, tablePKCols)
+ findIfView)
import PostgREST.DbStructure.Identifiers (FieldName,
QualifiedIdentifier (..),
Schema)
@@ -313,7 +313,7 @@ handleCreate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
let
ApiRequest{..} = ctxApiRequest
- pkCols = tablePKCols ctxDbStructure qiSchema qiName
+ pkCols = maybe mempty tablePKCols $ M.lookup identifier $ dbTables ctxDbStructure
WriteQueryResult{..} <- writeQuery MutationCreate identifier True pkCols context
@@ -433,7 +433,7 @@ handleInfo identifier RequestContext{..} =
++ ["DELETE" | tableDeletable table]
)
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 invMethod proc context@RequestContext{..} = do
@@ -537,7 +537,7 @@ writeQuery mutation identifier@QualifiedIdentifier{..} isInsert pkCols context@R
mutateReq <-
liftEither $
ReqBuilder.mutateRequest mutation qiSchema qiName ctxApiRequest
- (tablePKCols ctxDbStructure qiSchema qiName)
+ (maybe mempty tablePKCols $ M.lookup identifier $ dbTables ctxDbStructure)
readReq
(_, queryTotal, fields, body, gucHeaders, gucStatus) <-
diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs
index fadaf8dc4..657ab60b9 100644
--- a/src/PostgREST/DbStructure.hs
+++ b/src/PostgREST/DbStructure.hs
@@ -26,26 +26,24 @@ module PostgREST.DbStructure
, findIfView
, schemaDescription
, tableCols
- , tablePKCols
) where
import qualified Data.Aeson as JSON
import qualified Data.HashMap.Strict as M
-import qualified Data.List as L
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as SQL
import qualified Hasql.Transaction as SQL
import Contravariant.Extras (contrazip2)
-import Data.Set as S (fromList)
import Data.Text (split)
import Text.InterpolatedString.Perl6 (q)
import PostgREST.Config.Database (pgVersionStatement)
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
pgVersion110)
-import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
+import PostgREST.DbStructure.Identifiers (FieldName,
+ QualifiedIdentifier (..),
Schema, TableName)
import PostgREST.DbStructure.Proc (PgType (..),
ProcDescription (..),
@@ -54,20 +52,17 @@ import PostgREST.DbStructure.Proc (PgType (..),
ProcsMap, RetType (..))
import PostgREST.DbStructure.Relationship (Cardinality (..),
Junction (..),
- PrimaryKey (..),
Relationship (..))
import PostgREST.DbStructure.Table (Column (..), Table (..),
TablesMap)
import Protolude
-import Protolude.Unsafe (unsafeHead)
data DbStructure = DbStructure
{ dbTables :: TablesMap
, dbColumns :: [Column]
, dbRelationships :: [Relationship]
- , dbPrimaryKeys :: [PrimaryKey]
, dbProcs :: ProcsMap
}
deriving (Generic, JSON.ToJSON)
@@ -76,16 +71,22 @@ data DbStructure = DbStructure
tableCols :: DbStructure -> Schema -> TableName -> [Column]
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 identifier tbls = maybe False tableIsView $ M.lookup identifier tbls
--- | The source table column a view column refers to
-type SourceColumn = (Column, ViewColumn)
-type ViewColumn = Column
+-- | A view foreign key or primary key dependency detected on its source table
+data ViewKeyDependency = ViewKeyDependency {
+ 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
type SqlQuery = ByteString
@@ -96,19 +97,17 @@ queryDbStructure schemas extraSearchPath prepared = do
pgVer <- SQL.statement mempty pgVersionStatement
tabs <- SQL.statement mempty $ allTables pgVer prepared
cols <- SQL.statement schemas $ allColumns tabs prepared
- srcCols <- SQL.statement (schemas, extraSearchPath) $ pfkSourceColumns cols prepared
- m2oRels <- SQL.statement mempty $ allM2ORels pgVer cols prepared
- keys <- SQL.statement mempty $ allPrimaryKeys tabs prepared
+ keyDeps <- SQL.statement (schemas, extraSearchPath) $ allViewsKeyDependencies prepared
+ m2oRels <- SQL.statement mempty $ allM2ORels pgVer prepared
procs <- SQL.statement schemas $ allProcs pgVer prepared
- let rels = addO2MRels . addM2MRels $ addViewM2ORels srcCols m2oRels
- keys' = addViewPrimaryKeys srcCols keys
+ let rels = addO2MRels $ addM2MRels $ addViewM2ORels keyDeps m2oRels
+ tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
return $ removeInternal schemas $ DbStructure {
- dbTables = tabs
+ dbTables = tabsWViewsPks
, dbColumns = cols
, dbRelationships = rels
- , dbPrimaryKeys = keys'
, dbProcs = procs
}
@@ -121,7 +120,6 @@ removeInternal schemas dbStruct =
, dbRelationships = filter (\x -> qiSchema (relTable x) `elem` schemas &&
qiSchema (relForeignTable x) `elem` schemas &&
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.
}
where
@@ -140,6 +138,7 @@ decodeTables =
<*> column HD.bool
<*> column HD.bool
<*> column HD.bool
+ <*> arrayColumn HD.text
decodeColumns :: TablesMap -> HD.Result [Column]
decodeColumns tables =
@@ -157,40 +156,42 @@ decodeColumns tables =
<*> nullableColumn HD.text
<*> nullableColumn HD.text
-decodeRels :: [Column] -> HD.Result [Relationship]
-decodeRels cols =
- mapMaybe (relFromRow cols) <$> HD.rowList relRow
+decodeRels :: HD.Result [Relationship]
+decodeRels =
+ map relFromRow <$> HD.rowList relRow
where
- relRow = (,,,,,,)
- <$> column HD.text
+ relRow = (,,,,,)
+ <$> column HD.text <*> column HD.text
+ <*> column HD.text <*> column HD.text
<*> column HD.text
- <*> column HD.text
- <*> arrayColumn HD.text
- <*> column HD.text
- <*> column HD.text
- <*> arrayColumn HD.text
+ <*> compositeArrayColumn
+ ((,)
+ <$> compositeField HD.text
+ <*> compositeField HD.text)
-decodePks :: TablesMap -> HD.Result [PrimaryKey]
-decodePks tables =
- mapMaybe (pkFromRow tables) <$> HD.rowList pkRow
- where
- pkRow = (,,) <$> column HD.text <*> column HD.text <*> column HD.text
+relFromRow :: (Text, Text, Text, Text, Text, [(Text, Text)]) -> Relationship
+relFromRow (rs, rt, frs, frt, cn, cs) =
+ Relationship (QualifiedIdentifier rs rt) (QualifiedIdentifier frs frt) (M2O cn cs)
-decodeSourceColumns :: [Column] -> HD.Result [SourceColumn]
-decodeSourceColumns cols =
- mapMaybe (sourceColumnFromRow cols) <$> HD.rowList srcColRow
+decodeViewKeyDeps :: HD.Result [ViewKeyDependency]
+decodeViewKeyDeps =
+ map viewKeyDepFromRow <$> HD.rowList row
where
- srcColRow = (,,,,,)
+ row = (,,,,,,)
<$> 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
-sourceColumnFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
+viewKeyDepFromRow :: (Text,Text,Text,Text,Text,Text,[(Text, Text)]) -> ViewKeyDependency
+viewKeyDepFromRow (s1,t1,s2,v2,cons,consType,sCols) = ViewKeyDependency (QualifiedIdentifier s1 t1) (QualifiedIdentifier s2 v2) cons keyDep sCols
where
- col1 = findCol s1 t1 c1
- col2 = findCol s2 t2 c2
- findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols
+ keyDep | consType == "p" = PKDep
+ | consType == "f" = FKDep
+ | otherwise = FKDepRef -- f_ref, we build this type in the query
decodeProcs :: HD.Result ProcsMap
decodeProcs =
@@ -337,91 +338,71 @@ accessibleTables 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
-
- t1.c1----t2.c2 t1.c1----------t2.c2
- -> ________/
- /
- 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.
+--this function result--
+test | personnages_view | private | actors | personnages_role_id_fkey | f | {"(roleId,id)"} | viewTableM2O
+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
-}
-addViewM2ORels :: [SourceColumn] -> [Relationship] -> [Relationship]
-addViewM2ORels allSrcCols = concatMap (\rel@Relationship{..} -> rel :
- let
- srcColsGroupedByView :: [Column] -> [[SourceColumn]]
- srcColsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $
- filter (\(c, _) -> c `elem` relCols) allSrcCols
- relSrcCols = srcColsGroupedByView relColumns
- relFSrcCols = srcColsGroupedByView relForeignColumns
- getView :: [SourceColumn] -> QualifiedIdentifier
- getView = (\t -> QualifiedIdentifier (tableSchema t) (tableName t)) . colTable . snd . unsafeHead
- srcCols `allSrcColsOf` cols = S.fromList (fst <$> srcCols) == S.fromList cols
- -- Relationship is dependent on the order of relColumns and relFColumns to get the join conditions right in the generated query.
- -- So we need to change the order of the SourceColumns to match the relColumns
- -- TODO: This could be avoided if the Relationship type is improved with a structure that maintains the association of relColumns and relFColumns
- srcCols `sortAccordingTo` cols = sortOn (\(k, _) -> L.lookup k $ zip cols [0::Int ..]) srcCols
+addViewM2ORels :: [ViewKeyDependency] -> [Relationship] -> [Relationship]
+addViewM2ORels keyDeps rels =
+ rels ++ concat (viewRels <$> rels)
+ where
+ viewRels Relationship{relTable,relForeignTable,relCardinality=M2O cons relColumns} =
+ let
+ viewTableM2Os = filter (\ViewKeyDependency{keyDepTable, keyDepCons, keyDepType} -> keyDepTable == relTable && keyDepCons == cons && keyDepType == FKDep) keyDeps
+ tableViewM2Os = filter (\ViewKeyDependency{keyDepTable, keyDepCons, keyDepType} -> keyDepTable == relForeignTable && keyDepCons == cons && keyDepType == FKDepRef) keyDeps
+ in
+ [ Relationship
+ (keyDepView vwTbl)
+ relForeignTable
+ (M2O cons $ zipWith (\(_, vCol) (_, fCol)-> (vCol, fCol)) (keyDepCols vwTbl) relColumns)
+ | vwTbl <- viewTableM2Os ]
+ ++
+ [ 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 rels = rels ++ [ Relationship ft fc t c (O2M cons)
- | Relationship t c ft fc (M2O cons) <- rels ]
+addO2MRels rels = rels ++ [ Relationship ft t (O2M cons (swap <$> cols))
+ | Relationship t ft (M2O cons cols) <- rels ]
addM2MRels :: [Relationship] -> [Relationship]
-addM2MRels rels = rels ++ [ Relationship t c ft fc (M2M $ Junction jt1 cons1 jc1 cons2 jc2)
- | Relationship jt1 jc1 t c (M2O cons1) <- rels
- , Relationship jt2 jc2 ft fc (M2O cons2) <- rels
+addM2MRels rels = rels ++ [ Relationship t ft
+ (M2M $ Junction jt1 cons1 cons2 (swap <$> cols) (swap <$> fcols))
+ | Relationship jt1 t (M2O cons1 cols) <- rels
+ , Relationship jt2 ft (M2O cons2 fcols) <- rels
, jt1 == jt2
, cons1 /= cons2]
-addViewPrimaryKeys :: [SourceColumn] -> [PrimaryKey] -> [PrimaryKey]
-addViewPrimaryKeys srcCols = concatMap (\pk ->
- let viewPks = (\(_, viewCol) -> PrimaryKey{pkTable=colTable viewCol, pkName=colName viewCol}) <$>
- filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) srcCols in
- pk : viewPks)
+addViewPrimaryKeys :: TablesMap -> [ViewKeyDependency] -> TablesMap
+addViewPrimaryKeys tabs keyDeps =
+ (\tbl@Table{tableSchema, tableName, tableIsView}-> if tableIsView
+ then tbl{tablePKCols=findViewPKCols tableSchema tableName}
+ 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 pgVer =
@@ -429,8 +410,84 @@ allTables pgVer =
where
sql = tablesSqlQuery True pgVer
+-- | Gets tables with their PK cols
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
n.nspname AS table_schema,
c.relname AS table_name,
@@ -461,10 +518,12 @@ tablesSqlQuery getAll pgVer = [q|
-- CMD_DELETE
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
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')
AND n.nspname NOT IN ('pg_catalog', 'information_schema') |] <>
relIsPartition <>
@@ -613,23 +672,20 @@ columnFromRow tabs (s, t, n, desc, nul, typ, l, d, e) = buildColumn <$> table
parseEnum :: Maybe Text -> [Text]
parseEnum = maybe [] (split (==','))
-allM2ORels :: PgVersion -> [Column] -> Bool -> SQL.Statement () [Relationship]
-allM2ORels pgVer allCols =
- SQL.Statement sql HE.noParams (decodeRels allCols)
+allM2ORels :: PgVersion -> Bool -> SQL.Statement () [Relationship]
+allM2ORels pgVer =
+ SQL.Statement sql HE.noParams decodeRels
where
sql = [q|
SELECT ns1.nspname AS table_schema,
tab.relname AS table_name,
- conname AS constraint_name,
- column_info.cols AS columns,
ns2.nspname AS foreign_table_schema,
other.relname AS foreign_table_name,
- column_info.refs AS foreign_columns
+ conname AS constraint_name,
+ column_info.cols AS columns
FROM pg_constraint,
LATERAL (
- SELECT array_agg(cols.attname) AS cols,
- array_agg(cols.attnum) AS nums,
- array_agg(refs.attname) AS refs
+ SELECT array_agg(row(cols.attname, refs.attname) order by cols.attnum) AS cols
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 = confrelid AND attnum = ref) AS refs) AS column_info,
@@ -641,100 +697,12 @@ allM2ORels pgVer allCols =
(if pgVer >= pgVersion110
then " and conparentid = 0 "
else mempty) <>
- "ORDER BY (conrelid, column_info.nums)"
+ "ORDER BY conrelid, conname"
-relFromRow :: [Column] -> (Text, Text, Text, [Text], Text, Text, [Text]) -> Maybe Relationship
-relFromRow allCols (rs, rt, cn, rcs, frs, frt, frcs) =
- Relationship (QualifiedIdentifier rs rt) <$> cols <*> pure (QualifiedIdentifier frs frt) <*> colsF <*> pure (M2O cn)
- where
- 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)
+-- | Returns all the views' primary keys and foreign keys dependencies
+allViewsKeyDependencies :: Bool -> SQL.Statement ([Schema], [Schema]) [ViewKeyDependency]
+allViewsKeyDependencies =
+ SQL.Statement sql (contrazip2 (arrayParam HE.text) (arrayParam HE.text)) decodeViewKeyDeps
-- query explanation at:
-- * rationale: https://gist.github.com/wolfgangwalther/5425d64e7b0d20aad71f6f68474d9f19
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
@@ -744,6 +712,8 @@ pfkSourceColumns cols =
pks_fks as (
-- pk + fk referencing col
select
+ contype,
+ conname,
conrelid as resorigtbl,
unnest(conkey) as resorigcol
from pg_constraint
@@ -751,6 +721,8 @@ pfkSourceColumns cols =
union
-- fk referenced col
select
+ concat(contype, '_ref') as contype,
+ conname,
confrelid,
unnest(confkey)
from pg_constraint
@@ -879,17 +851,19 @@ pfkSourceColumns cols =
select
sch.nspname as table_schema,
tbl.relname as table_name,
- col.attname as table_column_name,
rec.view_schema,
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
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 vcol on vcol.attrelid = rec.view_id and vcol.attnum = rec.view_column
join pg_namespace sch on sch.oid = tbl.relnamespace
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.param . HE.nonNullable
diff --git a/src/PostgREST/DbStructure/Relationship.hs b/src/PostgREST/DbStructure/Relationship.hs
index 000f018bb..4f0fcc09a 100644
--- a/src/PostgREST/DbStructure/Relationship.hs
+++ b/src/PostgREST/DbStructure/Relationship.hs
@@ -3,7 +3,6 @@
module PostgREST.DbStructure.Relationship
( Cardinality(..)
- , PrimaryKey(..)
, Relationship(..)
, Junction(..)
, isSelfReference
@@ -11,24 +10,17 @@ module PostgREST.DbStructure.Relationship
import qualified Data.Aeson as JSON
-import PostgREST.DbStructure.Identifiers (QualifiedIdentifier)
-import PostgREST.DbStructure.Table (Column (..), Table)
+import PostgREST.DbStructure.Identifiers (FieldName,
+ QualifiedIdentifier)
import Protolude
-- | 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
- { relTable :: QualifiedIdentifier
- , relColumns :: [Column]
- , relForeignTable :: QualifiedIdentifier
- , relForeignColumns :: [Column]
- , relCardinality :: Cardinality
+ { relTable :: QualifiedIdentifier
+ , relForeignTable :: QualifiedIdentifier
+ , relCardinality :: Cardinality
}
deriving (Eq, Generic, JSON.ToJSON)
@@ -36,9 +28,12 @@ data Relationship = Relationship
-- | https://en.wikipedia.org/wiki/Cardinality_(data_modeling)
-- TODO: missing one-to-one
data Cardinality
- = O2M FKConstraint -- ^ one-to-many cardinality
- | M2O FKConstraint -- ^ many-to-one cardinality
- | M2M Junction -- ^ many-to-many cardinality
+ = O2M {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}
+ -- ^ one-to-many
+ | M2O {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}
+ -- ^ many-to-one
+ | M2M Junction
+ -- ^ many-to-many
deriving (Eq, Generic, JSON.ToJSON)
type FKConstraint = Text
@@ -47,17 +42,11 @@ type FKConstraint = Text
data Junction = Junction
{ junTable :: QualifiedIdentifier
, junConstraint1 :: FKConstraint
- , junColumns1 :: [Column]
, junConstraint2 :: FKConstraint
- , junColumns2 :: [Column]
+ , junColumns1 :: [(FieldName, FieldName)]
+ , junColumns2 :: [(FieldName, FieldName)]
}
deriving (Eq, Generic, JSON.ToJSON)
isSelfReference :: Relationship -> Bool
isSelfReference r = relTable r == relForeignTable r
-
-data PrimaryKey = PrimaryKey
- { pkTable :: Table
- , pkName :: Text
- }
- deriving (Generic, JSON.ToJSON)
diff --git a/src/PostgREST/DbStructure/Table.hs b/src/PostgREST/DbStructure/Table.hs
index 9f48a1f91..0fdc7c7de 100644
--- a/src/PostgREST/DbStructure/Table.hs
+++ b/src/PostgREST/DbStructure/Table.hs
@@ -27,6 +27,7 @@ data Table = Table
, tableInsertable :: Bool
, tableUpdatable :: Bool
, tableDeletable :: Bool
+ , tablePKCols :: [FieldName]
}
deriving (Show, Ord, Generic, JSON.ToJSON)
diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs
index 2847a1e90..951a71ecd 100644
--- a/src/PostgREST/Error.hs
+++ b/src/PostgREST/Error.hs
@@ -40,8 +40,6 @@ import PostgREST.DbStructure.Proc (ProcDescription (..),
import PostgREST.DbStructure.Relationship (Cardinality (..),
Junction (..),
Relationship (..))
-import PostgREST.DbStructure.Table (Column (..))
-
import Protolude
@@ -158,15 +156,15 @@ compressedRel Relationship{..} =
: case relCardinality of
M2M Junction{..} -> [
"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)
- , "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)
- , "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
@@ -176,8 +174,8 @@ relHint rels = T.intercalate ", " (hintList <$> rels)
let buildHint rel = "'" <> qiName relForeignTable <> "!" <> rel <> "'" in
case relCardinality of
M2M Junction{..} -> buildHint (qiName junTable)
- M2O cons -> buildHint cons
- O2M cons -> buildHint cons
+ M2O cons _ -> buildHint cons
+ O2M cons _ -> buildHint cons
data PgError = PgError Authenticated SQL.UsageError
type Authenticated = Bool
diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs
index 7cd174819..76aa3a6d6 100644
--- a/src/PostgREST/OpenAPI.hs
+++ b/src/PostgREST/OpenAPI.hs
@@ -3,7 +3,6 @@ Module : PostgREST.OpenAPI
Description : Generates the OpenAPI output
-}
{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
module PostgREST.OpenAPI (encode) where
@@ -28,12 +27,11 @@ import Data.Swagger
import PostgREST.Config (AppConfig (..), Proxy (..),
isMalformedProxyUri, toURI)
import PostgREST.DbStructure (DbStructure (..),
- tableCols, tablePKCols)
+ tableCols)
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
import PostgREST.DbStructure.Proc (ProcDescription (..),
ProcParam (..))
import PostgREST.DbStructure.Relationship (Cardinality (..),
- PrimaryKey (..),
Relationship (..))
import PostgREST.DbStructure.Table (Column (..), Table (..),
TablesMap)
@@ -52,7 +50,6 @@ encode conf dbStructure tables procs schemaDescription =
(openApiTableInfo dbStructure <$> (snd <$> M.toList tables))
(proxyUri conf)
schemaDescription
- (dbPrimaryKeys dbStructure)
makeMimeList :: [ContentType] -> MimeList
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
@@ -83,34 +80,34 @@ parseDefault colType colDefault =
where
wrapInQuotations text = "\"" <> text <> "\""
-makeTableDef :: [Relationship] -> [PrimaryKey] -> (Table, [Column], [Text]) -> (Text, Schema)
-makeTableDef rels pks (t, cs, _) =
+makeTableDef :: [Relationship] -> (Table, [Column]) -> (Text, Schema)
+makeTableDef rels (t, cs) =
let tn = tableName t in
(tn, (mempty :: Schema)
& description .~ tableDescription t
& type_ ?~ SwaggerObject
- & properties .~ fromList (fmap (makeProperty rels pks) cs)
+ & properties .~ fromList (fmap (makeProperty rels) cs)
& required .~ fmap colName (filter (not . colNullable) cs))
-makeProperty :: [Relationship] -> [PrimaryKey] -> Column -> (Text, Referenced Schema)
-makeProperty rels pks c = (colName c, Inline s)
+makeProperty :: [Relationship] -> Column -> (Text, Referenced Schema)
+makeProperty rels col = (colName col, Inline s)
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 =
let
-- Finds the relationship that has a single column foreign key
rel = find (\case
- Relationship{relColumns, relCardinality=M2O _} -> [c] == relColumns
- _ -> False
+ Relationship{relCardinality=(M2O _ relColumns)} -> [colName col] == (fst <$> relColumns)
+ _ -> False
) rels
- fCol = colName <$> (headMay . relForeignColumns =<< rel)
+ fCol = (headMay . (\r -> snd <$> relColumns (relCardinality r)) =<< rel)
fTbl = qiName . relForeignTable <$> rel
fTblCol = (,) <$> fTbl <*> fCol
in
(\(a, b) -> T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`."]) <$> fTblCol
pk :: Bool
- pk = any (\p -> pkTable p == colTable c && pkName p == colName c) pks
+ pk = colName col `elem` tablePKCols (colTable col)
n = catMaybes
[ Just "Note:"
, if pk then Just "This is a Primary Key." else Nothing
@@ -118,17 +115,17 @@ makeProperty rels pks c = (colName c, Inline s)
]
d =
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
- colDescription c
+ colDescription col
s =
(mempty :: Schema)
- & default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType c) =<< colDefault c)
+ & default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
& description .~ d
& enum_ .~ e
- & format ?~ colType c
- & maxLength .~ (fromIntegral <$> colMaxLen c)
- & type_ .~ toSwaggerType (colType c)
+ & format ?~ colType col
+ & maxLength .~ (fromIntegral <$> colMaxLen col)
+ & type_ .~ toSwaggerType (colType col)
makeProcSchema :: ProcDescription -> Schema
makeProcSchema pd =
@@ -165,7 +162,7 @@ makeProcParam pd =
, Ref $ Reference "preferParams"
]
-makeParamDefs :: [(Table, [Column], [Text])] -> [(Text, Param)]
+makeParamDefs :: [(Table, [Column])] -> [(Text, Param)]
makeParamDefs ti =
[ ("preferParams", makePreferParam ["params=single-object"])
, ("preferReturn", makePreferParam ["return=representation", "return=minimal", "return=none"])
@@ -222,7 +219,7 @@ makeParamDefs ti =
& type_ ?~ SwaggerString))
]
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) cs
- | (t, cs, _) <- ti
+ | (t, cs) <- ti
]
makeObjectBody :: Text -> (Text, Param)
@@ -247,8 +244,8 @@ makeRowFilter tn c =
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
makeRowFilters tn = fmap (makeRowFilter tn)
-makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
-makePathItem (t, cs, _) = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t)
+makePathItem :: (Table, [Column]) -> (FilePath, PathItem)
+makePathItem (t, cs) = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t)
where
-- 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
@@ -312,7 +309,7 @@ makeRootPathItem = ("/", p)
pr = (mempty :: PathItem) & get ?~ getOp
p = pr
-makePathItems :: [ProcDescription] -> [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
+makePathItems :: [ProcDescription] -> [(Table, [Column])] -> InsOrdHashMap FilePath PathItem
makePathItems pds ti = fromList $ makeRootPathItem :
fmap makePathItem ti ++ fmap makeProcPathItem pds
@@ -324,8 +321,8 @@ escapeHostName "*6" = "0.0.0.0"
escapeHostName "!6" = "0.0.0.0"
escapeHostName h = h
-postgrestSpec :: [Relationship] -> [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> Swagger
-postgrestSpec rels pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
+postgrestSpec :: [Relationship] -> [ProcDescription] -> [(Table, [Column])] -> (Text, Text, Integer, Text) -> Maybe Text -> Swagger
+postgrestSpec rels pds ti (s, h, p, b) sd = (mempty :: Swagger)
& basePath ?~ T.unpack b
& schemes ?~ [s']
& info .~ ((mempty :: Info)
@@ -336,7 +333,7 @@ postgrestSpec rels pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
& description ?~ "PostgREST Documentation"
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
& host .~ h'
- & definitions .~ fromList (makeTableDef rels pks <$> ti)
+ & definitions .~ fromList (makeTableDef rels <$> ti)
& parameters .~ fromList (makeParamDefs ti)
& paths .~ makePathItems pds ti
& produces .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
@@ -383,9 +380,8 @@ proxyUri AppConfig{..} =
Nothing ->
("http", configServerHost, toInteger configServerPort, "/")
-openApiTableInfo :: DbStructure -> Table -> (Table, [Column], [Text])
+openApiTableInfo :: DbStructure -> Table -> (Table, [Column])
openApiTableInfo dbStructure table =
( table
, tableCols dbStructure (tableSchema table) (tableName table)
- , tablePKCols dbStructure (tableSchema table) (tableName table)
)
diff --git a/src/PostgREST/Query/QueryBuilder.hs b/src/PostgREST/Query/QueryBuilder.hs
index cedd68b09..3353fb4f2 100644
--- a/src/PostgREST/Query/QueryBuilder.hs
+++ b/src/PostgREST/Query/QueryBuilder.hs
@@ -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) =
let subquery = readRequestToQuery rr in
case card of
- M2O _ ->
+ M2O _ _ ->
let aliasOrName = fromMaybe name alias
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
sel = SQL.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName)
diff --git a/src/PostgREST/Request/DbRequestBuilder.hs b/src/PostgREST/Request/DbRequestBuilder.hs
index 3b5107d13..a33da37ef 100644
--- a/src/PostgREST/Request/DbRequestBuilder.hs
+++ b/src/PostgREST/Request/DbRequestBuilder.hs
@@ -37,7 +37,6 @@ import PostgREST.DbStructure.Proc (ProcDescription (..),
import PostgREST.DbStructure.Relationship (Cardinality (..),
Junction (..),
Relationship (..))
-import PostgREST.DbStructure.Table (Column (..))
import PostgREST.Error (Error (..))
import PostgREST.Query.SqlFragment (sourceCTEName)
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
-- 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
- (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
+ (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
_ -> Left $ AmbiguousRelBetween origin target rs
rs -> Left $ AmbiguousRelBetween origin target rs
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
- O2M cons -> tar == Just cons
- M2O cons -> tar == Just cons
- _ -> False
+ O2M cons _ -> tar == Just cons
+ M2O cons _ -> tar == Just cons
+ _ -> False
matchJunction hint_ card = case card of
M2M Junction{junTable} -> hint_ == Just (qiName junTable)
_ -> False
@@ -192,8 +198,8 @@ findRel schema allRels origin target hint =
) ||
-- /projects?select=client_id(*)
(
- origin == qiName relTable && -- projects
- matchFKSingleCol (Just target) relColumns -- client_id
+ origin == qiName relTable && -- projects
+ matchFKSingleCol (Just target) relCardinality -- client_id
)
) && (
isNothing hint || -- hint is optional
@@ -202,8 +208,8 @@ findRel schema allRels origin target hint =
matchConstraint hint relCardinality || -- projects_client_id_fkey
-- /projects?select=clients!client_id(*) or /projects?select=clients!id(*)
- matchFKSingleCol hint relColumns || -- client_id
- matchFKSingleCol hint relForeignColumns || -- id
+ matchFKSingleCol hint relCardinality || -- client_id
+ matchFKRefSingleCol hint relCardinality || -- id
-- /users?select=tasks!users_tasks(*) many-to-many between users and 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
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
- M2M (Junction QualifiedIdentifier{qiName=jtn} _ jc1 _ jc2) ->
- zipWith (toJoinCondition tN jtn) cols jc1 ++ zipWith (toJoinCondition ftN jtn) fCols jc2
- _ ->
- zipWith (toJoinCondition tN ftN) cols fCols
+ M2M (Junction QualifiedIdentifier{qiName=jtn} _ _ jcols1 jcols2) ->
+ (toJoinCondition tN jtn <$> jcols1) ++ (toJoinCondition ftN jtn <$> jcols2)
+ O2M _ cols ->
+ toJoinCondition tN ftN <$> cols
+ M2O _ cols ->
+ toJoinCondition tN ftN <$> cols
where
- toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
- toJoinCondition tb ftb c fc =
+ toJoinCondition :: Text -> Text -> (FieldName, FieldName) -> JoinCondition
+ toJoinCondition tb ftb (c, fc) =
let qi1 = removeSourceCTESchema tSchema tb
qi2 = removeSourceCTESchema tSchema ftb in
- JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, colName c)
- (maybe qi2 (QualifiedIdentifier mempty) newAlias, colName fc)
+ JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, c)
+ (maybe qi2 (QualifiedIdentifier mempty) newAlias, fc)
-- On mutation and calling proc cases we wrap the target table in a WITH
-- {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
-- succeeds, result would be `RETURNING name, client_id`.
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
) forest
-- 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
-- make sure, that a proper location header can always be built for
-- 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
-- they are later concatenated with AND in the QueryBuilder
diff --git a/test/spec/Feature/OpenApi/RootSpec.hs b/test/spec/Feature/OpenApi/RootSpec.hs
index 4f460cbd3..e894f54d0 100644
--- a/test/spec/Feature/OpenApi/RootSpec.hs
+++ b/test/spec/Feature/OpenApi/RootSpec.hs
@@ -27,6 +27,6 @@ spec =
request methodGet "/"
[("Accept", "application/json")] "" `shouldRespondWith`
[json| {
- "qiSchema":"test","qiName":"orders_view"
+ "qiSchema":"test","qiName":"has_fk"
} |]
{ matchHeaders = [matchContentTypeJson] }
diff --git a/test/spec/Feature/Query/InsertSpec.hs b/test/spec/Feature/Query/InsertSpec.hs
index 57622a223..7a8caddca 100644
--- a/test/spec/Feature/Query/InsertSpec.hs
+++ b/test/spec/Feature/Query/InsertSpec.hs
@@ -595,7 +595,7 @@ spec actualPgVersion = 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")]
[json|{"k1":1,"k2":"test","extra":2}|]
`shouldRespondWith`
@@ -606,13 +606,13 @@ spec actualPgVersion = do
, "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")]
[json|{"id":1}|]
`shouldRespondWith`
""
{ matchStatus = 201
, 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" <:> "*/*" ]
}