Merge @ruslantalpa's fk improved detection

This commit is contained in:
Joe Nelson
2016-03-12 12:42:57 -08:00
13 changed files with 183 additions and 105 deletions
+3 -2
View File
@@ -6,8 +6,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
### Fixed ### Fixed
* Allow SQL functions to generate registered JWT claims - @begriffs - Allow SQL functions to generate registered JWT claims - @begriffs
* Terminate gracefully on SIGTERM (for use in Docker) - @recmo - Terminate gracefully on SIGTERM (for use in Docker) - @recmo
- Relation detection fix for views that depend on multiple tables - @ruslantalpa
## [0.3.1.0] - 2016-02-28 ## [0.3.1.0] - 2016-02-28
+5 -4
View File
@@ -108,12 +108,13 @@ library
, time , time
, unordered-containers , unordered-containers
, vector , vector
, wai
, wai-cors
, wai-extra
, wai-middleware-static
, HTTP , HTTP
, Ranged-sets , Ranged-sets
, wai >= 3.0.1
, wai-cors
, wai-extra
, wai-middleware-static >= 0.6.0
, warp >= 3.1.0
Other-Modules: Paths_postgrest Other-Modules: Paths_postgrest
Exposed-Modules: PostgREST.App Exposed-Modules: PostgREST.App
+1 -1
View File
@@ -30,7 +30,7 @@ import Data.Monoid ((<>))
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text (Text) import Data.Text (Text)
import Data.Time.Clock (NominalDiffTime) import Data.Time.Clock (NominalDiffTime)
import PostgREST.QueryBuilder (pgFmtLit, pgFmtIdent, unquoted) import PostgREST.QueryBuilder (pgFmtIdent, pgFmtLit, unquoted)
import qualified Web.JWT as JWT import qualified Web.JWT as JWT
{-| {-|
+1 -1
View File
@@ -30,9 +30,9 @@ import Network.Wai
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..)) import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Options.Applicative import Options.Applicative
import Paths_postgrest (version) import Paths_postgrest (version)
import Prelude
import Safe (readMay) import Safe (readMay)
import Web.JWT (Secret, secret) import Web.JWT (Secret, secret)
import Prelude
-- | Data type to store all command line options -- | Data type to store all command line options
data AppConfig = AppConfig { data AppConfig = AppConfig {
+81 -72
View File
@@ -10,23 +10,25 @@ module PostgREST.DbStructure (
, doesProcReturnJWT , doesProcReturnJWT
) where ) where
import qualified Hasql.Query as H import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE import qualified Hasql.Encoders as HE
import qualified Hasql.Decoders as HD import qualified Hasql.Query as H
import Control.Applicative import Control.Applicative
import Control.Monad (join, replicateM) import Control.Monad (join, replicateM)
import Data.Functor.Contravariant (contramap) import Data.Functor.Contravariant (contramap)
import Text.InterpolatedString.Perl6 (q) import Data.List (elemIndex, find, sort,
import Data.List (elemIndex, find, subsequences, sort, transpose) subsequences, transpose)
import Data.Maybe (fromMaybe, fromJust, isJust, mapMaybe, listToMaybe) import Data.Maybe (fromJust, fromMaybe, isJust,
listToMaybe, mapMaybe)
import Data.Monoid import Data.Monoid
import Data.Text (Text, split) import Data.Text (Text, split)
import qualified Hasql.Session as H import qualified Hasql.Session as H
import PostgREST.Types import PostgREST.Types
import Text.InterpolatedString.Perl6 (q)
import GHC.Exts (groupWith) import Data.Int (Int32)
import Data.Int (Int32) import GHC.Exts (groupWith)
import Prelude import Prelude
getDbStructure :: Schema -> H.Session DbStructure getDbStructure :: Schema -> H.Session DbStructure
@@ -556,69 +558,76 @@ allSynonyms :: [Column] -> H.Query () [(Column,Column)]
allSynonyms cols = allSynonyms cols =
H.statement sql HE.unit (decodeSynonyms cols) True H.statement sql HE.unit (decodeSynonyms cols) True
where where
-- query explanation at https://gist.github.com/ruslantalpa/2eab8c930a65e8043d8f
sql = [q| sql = [q|
WITH synonyms AS ( WITH view_columns AS (
/* SELECT
-- CTE to replace the view from information_schema because the information in it depended on the logged in role c.oid AS view_oid,
-- notice the commented line a.attname::information_schema.sql_identifier AS column_name
*/ FROM pg_attribute a
WITH view_column_usage AS ( JOIN pg_class c ON a.attrelid = c.oid
SELECT DISTINCT JOIN pg_namespace nc ON c.relnamespace = nc.oid
CAST(current_database() AS character varying) AS view_catalog, WHERE
CAST(nv.nspname AS character varying) AS view_schema, NOT pg_is_other_temp_schema(nc.oid)
CAST(v.relname AS character varying) AS view_name, AND a.attnum > 0
CAST(current_database() AS character varying) AS table_catalog, AND NOT a.attisdropped
CAST(nt.nspname AS character varying) AS table_schema, AND (c.relkind = 'v'::"char")
CAST(t.relname AS character varying) AS table_name, AND nc.nspname NOT IN ('information_schema', 'pg_catalog')
CAST(a.attname AS character varying) AS column_name ),
FROM pg_namespace nv, pg_class v, pg_depend dv, view_column_usage AS (
pg_depend dt, pg_class t, pg_namespace nt, SELECT DISTINCT
pg_attribute a v.oid as view_oid,
WHERE nv.oid = v.relnamespace nv.nspname::information_schema.sql_identifier AS view_schema,
AND v.relkind = 'v' v.relname::information_schema.sql_identifier AS view_name,
AND v.oid = dv.refobjid nt.nspname::information_schema.sql_identifier AS table_schema,
AND dv.refclassid = 'pg_catalog.pg_class'::regclass t.relname::information_schema.sql_identifier AS table_name,
AND dv.classid = 'pg_catalog.pg_rewrite'::regclass a.attname::information_schema.sql_identifier AS column_name,
AND dv.deptype = 'i' pg_get_viewdef(v.oid)::information_schema.character_data AS view_definition
AND dv.objid = dt.objid FROM pg_namespace nv
AND dv.refobjid <> dt.refobjid JOIN pg_class v ON nv.oid = v.relnamespace
AND dt.classid = 'pg_catalog.pg_rewrite'::regclass JOIN pg_depend dv ON v.oid = dv.refobjid
AND dt.refclassid = 'pg_catalog.pg_class'::regclass JOIN pg_depend dt ON dv.objid = dt.objid
AND dt.refobjid = t.oid JOIN pg_class t ON dt.refobjid = t.oid
AND t.relnamespace = nt.oid JOIN pg_namespace nt ON t.relnamespace = nt.oid
AND t.relkind IN ('r', 'v', 'f') JOIN pg_attribute a ON t.oid = a.attrelid AND dt.refobjsubid = a.attnum
AND t.oid = a.attrelid
AND dt.refobjsubid = a.attnum WHERE
/*--AND pg_has_role(t.relowner, 'USAGE')*/ nv.nspname not in ('information_schema', 'pg_catalog')
) AND v.relkind = 'v'::"char"
SELECT AND dv.refclassid = 'pg_class'::regclass::oid
vcu.table_schema AS src_table_schema, AND dv.classid = 'pg_rewrite'::regclass::oid
vcu.table_name AS src_table_name, AND dv.deptype = 'i'::"char"
vcu.column_name AS src_column_name, AND dv.refobjid <> dt.refobjid
view.schemaname AS syn_table_schema, AND dt.classid = 'pg_rewrite'::regclass::oid
view.viewname AS syn_table_name, AND dt.refclassid = 'pg_class'::regclass::oid
view.definition AS view_definition AND (t.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
FROM ),
pg_catalog.pg_views AS view, candidates AS (
view_column_usage AS vcu SELECT
WHERE vcu.*,
view.schemaname = vcu.view_schema AND (
view.viewname = vcu.view_name AND SELECT CASE WHEN match IS NOT NULL THEN coalesce(match[7], match[4]) END
view.schemaname NOT IN ('pg_catalog', 'information_schema') FROM REGEXP_MATCHES(
/*--AND (SELECT COUNT(*) FROM information_schema.view_table_usage WHERE view_schema = view.schemaname AND view_name = view.viewname) = 1*/ CONCAT('SELECT ', SPLIT_PART(vcu.view_definition, 'SELECT', 2)),
CONCAT('SELECT.*?((',vcu.table_name,')|(\w+))\.(', vcu.column_name, ')(\sAS\s(")?([^"]+)\6)?.*?FROM.*?',vcu.table_schema,'\.(\2|',vcu.table_name,'\s+(AS\s)?\3)'),
'ns'
) match
) AS view_column_name
FROM view_column_usage AS vcu
) )
SELECT SELECT
src_table_schema, src_table_name, src_column_name, c.table_schema,
syn_table_schema, syn_table_name, c.table_name,
(regexp_matches(view_definition, CONCAT('\.(', src_column_name, ')(?=,|$)'), 'gn'))[1] AS syn_column_name c.column_name AS table_column_name,
FROM synonyms c.view_schema,
UNION ( c.view_name,
SELECT c.view_column_name
src_table_schema, src_table_name, src_column_name, FROM view_columns AS vc, candidates AS c
syn_table_schema, syn_table_name, WHERE
(regexp_matches(view_definition, CONCAT('\.', src_column_name, '\sAS\s("?)(.+?)\1(,|$)'), 'gn'))[2] AS syn_column_name /* " <- for syntax highlighting */ vc.view_oid = c.view_oid AND
FROM synonyms vc.column_name = c.view_column_name
) |] ORDER BY c.view_schema, c.view_name, c.table_name, c.view_column_name
|]
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column) synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column)
synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2 synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
+1 -1
View File
@@ -12,8 +12,8 @@ import Data.Monoid ((<>))
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Hasql.Session as H
import qualified Hasql.Pool as P import qualified Hasql.Pool as P
import qualified Hasql.Session as H
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
import qualified Network.HTTP.Types.Status as HT import qualified Network.HTTP.Types.Status as HT
import Network.Wai (Response, responseLBS) import Network.Wai (Response, responseLBS)
+4 -4
View File
@@ -7,15 +7,15 @@ import Control.Monad (unless)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Text
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text
import Data.Time.Clock (NominalDiffTime) import Data.Time.Clock (NominalDiffTime)
import qualified Hasql.Transaction as H import qualified Hasql.Transaction as H
import Network.HTTP.Types.Header (hAccept, hAuthorization) import Network.HTTP.Types.Header (hAccept, hAuthorization)
import Network.HTTP.Types.Status (status415, status400) import Network.HTTP.Types.Status (status400, status415)
import Network.Wai (Application, Request (..), Response, import Network.Wai (Application, Request (..),
requestHeaders) Response, requestHeaders)
import Network.Wai.Middleware.Cors (cors) import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Gzip (def, gzip) import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.Static (only, staticPolicy) import Network.Wai.Middleware.Static (only, staticPolicy)
+2 -2
View File
@@ -3,14 +3,14 @@ module PostgREST.Parsers
-- ) -- )
where where
import Control.Applicative hiding ((<$>)) import Control.Applicative hiding ((<$>))
import Data.Monoid import Data.Monoid
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text (Text) import Data.Text (Text)
import Data.Tree import Data.Tree
import PostgREST.QueryBuilder (operators)
import PostgREST.Types import PostgREST.Types
import Text.ParserCombinators.Parsec hiding (many, (<|>)) import Text.ParserCombinators.Parsec hiding (many, (<|>))
import PostgREST.QueryBuilder (operators)
pRequestSelect :: Text -> Parser ReadRequest pRequestSelect :: Text -> Parser ReadRequest
pRequestSelect rootNodeName = do pRequestSelect rootNodeName = do
+8 -8
View File
@@ -1,16 +1,16 @@
module PostgREST.Types where module PostgREST.Types where
import Data.Text import Data.Aeson
import Data.Tree
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import Data.Int (Int32)
import Data.Text
import Data.Tree
import qualified Data.Vector as V import qualified Data.Vector as V
import Data.Aeson
import Data.Int (Int32)
data DbStructure = DbStructure { data DbStructure = DbStructure {
dbTables :: [Table] dbTables :: [Table]
, dbColumns :: [Column] , dbColumns :: [Column]
, dbRelations :: [Relation] , dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey] , dbPrimaryKeys :: [PrimaryKey]
} deriving (Show, Eq) } deriving (Show, Eq)
+1 -1
View File
@@ -9,7 +9,7 @@ extra-deps:
- postgresql-error-codes-1 - postgresql-error-codes-1
- postgresql-binary-0.8.1 - postgresql-binary-0.8.1
ghc-options: ghc-options:
postgrest: -O1 -Werror -Wall -fwarn-monomorphism-restriction -fwarn-missing-exported-sigs -fwarn-identities postgrest: -O2 -Werror -Wall -fwarn-monomorphism-restriction -fwarn-missing-exported-sigs -fwarn-identities
packages: packages:
- '.' - '.'
+56
View File
@@ -24,6 +24,7 @@ spec = do
, {"schema":"test","name":"comments","insertable":true} , {"schema":"test","name":"comments","insertable":true}
, {"schema":"test","name":"complex_items","insertable":true} , {"schema":"test","name":"complex_items","insertable":true}
, {"schema":"test","name":"compound_pk","insertable":true} , {"schema":"test","name":"compound_pk","insertable":true}
, {"schema":"test","name":"filtered_tasks","insertable":true}
, {"schema":"test","name":"ghostBusters","insertable":true} , {"schema":"test","name":"ghostBusters","insertable":true}
, {"schema":"test","name":"has_count_column","insertable":false} , {"schema":"test","name":"has_count_column","insertable":false}
, {"schema":"test","name":"has_fk","insertable":true} , {"schema":"test","name":"has_fk","insertable":true}
@@ -57,6 +58,61 @@ spec = do
{matchStatus = 200} {matchStatus = 200}
describe "Table info" $ do describe "Table info" $ do
it "The structure of complex views is correctly detected" $
request methodOptions "/filtered_tasks" [] "" `shouldRespondWith`
[json|
{
"pkey": [
"myId"
],
"columns": [
{
"references": null,
"default": null,
"precision": 32,
"updatable": true,
"schema": "test",
"name": "myId",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 1
},
{
"references": null,
"default": null,
"precision": null,
"updatable": true,
"schema": "test",
"name": "name",
"type": "text",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 2
},
{
"references": {
"schema": "test",
"column": "id",
"table": "projects"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "test",
"name": "projectID",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 3
}
]
}
|]
it "is available with OPTIONS verb" $ it "is available with OPTIONS verb" $
request methodOptions "/menagerie" [] "" `shouldRespondWith` request methodOptions "/menagerie" [] "" `shouldRespondWith`
[json| [json|
+1
View File
@@ -28,6 +28,7 @@ GRANT ALL ON TABLE
, projects_view , projects_view
, simple_pk , simple_pk
, tasks , tasks
, filtered_tasks
, tsearch , tsearch
, users , users
, users_projects , users_projects
+19 -9
View File
@@ -594,6 +594,15 @@ CREATE TABLE simple_pk (
extra character varying NOT NULL extra character varying NOT NULL
); );
--
-- Name: users_projects; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE users_projects (
user_id integer NOT NULL,
project_id integer NOT NULL
);
-- --
-- Name: tasks; Type: TABLE; Schema: test; Owner: - -- Name: tasks; Type: TABLE; Schema: test; Owner: -
@@ -605,6 +614,16 @@ CREATE TABLE tasks (
project_id integer project_id integer
); );
CREATE OR REPLACE VIEW filtered_tasks AS
SELECT id AS "myId", name, project_id AS "projectID"
FROM tasks
WHERE project_id IN (
SELECT id FROM projects WHERE id = 1
) AND
project_id IN (
SELECT project_id FROM users_projects WHERE user_id = 1
);
-- --
-- Name: tsearch; Type: TABLE; Schema: test; Owner: - -- Name: tsearch; Type: TABLE; Schema: test; Owner: -
@@ -625,15 +644,6 @@ CREATE TABLE users (
); );
--
-- Name: users_projects; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE users_projects (
user_id integer NOT NULL,
project_id integer NOT NULL
);
-- --
-- Name: users_tasks; Type: TABLE; Schema: test; Owner: - -- Name: users_tasks; Type: TABLE; Schema: test; Owner: -