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:
steve-chavez
2022-04-21 13:19:38 -05:00
parent 057e8d63bf
commit d5b92a433a
11 changed files with 290 additions and 321 deletions
+1
View File
@@ -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
+4 -4
View File
@@ -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
View File
@@ -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
+13 -24
View File
@@ -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)
+1
View File
@@ -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)
+7 -9
View File
@@ -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
View File
@@ -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)
) )
+1 -1
View File
@@ -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)
+32 -22
View File
@@ -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
+1 -1
View File
@@ -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] }
+3 -3
View File
@@ -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" <:> "*/*" ]
} }