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
### Fixed
* Allow SQL functions to generate registered JWT claims - @begriffs
* Terminate gracefully on SIGTERM (for use in Docker) - @recmo
- Allow SQL functions to generate registered JWT claims - @begriffs
- 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
+5 -4
View File
@@ -108,12 +108,13 @@ library
, time
, unordered-containers
, vector
, wai
, wai-cors
, wai-extra
, wai-middleware-static
, HTTP
, Ranged-sets
, wai >= 3.0.1
, wai-cors
, wai-extra
, wai-middleware-static >= 0.6.0
, warp >= 3.1.0
Other-Modules: Paths_postgrest
Exposed-Modules: PostgREST.App
+1 -1
View File
@@ -30,7 +30,7 @@ import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Time.Clock (NominalDiffTime)
import PostgREST.QueryBuilder (pgFmtLit, pgFmtIdent, unquoted)
import PostgREST.QueryBuilder (pgFmtIdent, pgFmtLit, unquoted)
import qualified Web.JWT as JWT
{-|
+1 -1
View File
@@ -30,9 +30,9 @@ import Network.Wai
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Options.Applicative
import Paths_postgrest (version)
import Prelude
import Safe (readMay)
import Web.JWT (Secret, secret)
import Prelude
-- | Data type to store all command line options
data AppConfig = AppConfig {
+81 -72
View File
@@ -10,23 +10,25 @@ module PostgREST.DbStructure (
, doesProcReturnJWT
) where
import qualified Hasql.Query as H
import qualified Hasql.Encoders as HE
import qualified Hasql.Decoders as HD
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Query as H
import Control.Applicative
import Control.Monad (join, replicateM)
import Data.Functor.Contravariant (contramap)
import Text.InterpolatedString.Perl6 (q)
import Data.List (elemIndex, find, subsequences, sort, transpose)
import Data.Maybe (fromMaybe, fromJust, isJust, mapMaybe, listToMaybe)
import Control.Monad (join, replicateM)
import Data.Functor.Contravariant (contramap)
import Data.List (elemIndex, find, sort,
subsequences, transpose)
import Data.Maybe (fromJust, fromMaybe, isJust,
listToMaybe, mapMaybe)
import Data.Monoid
import Data.Text (Text, split)
import qualified Hasql.Session as H
import Data.Text (Text, split)
import qualified Hasql.Session as H
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
getDbStructure :: Schema -> H.Session DbStructure
@@ -556,69 +558,76 @@ allSynonyms :: [Column] -> H.Query () [(Column,Column)]
allSynonyms cols =
H.statement sql HE.unit (decodeSynonyms cols) True
where
-- query explanation at https://gist.github.com/ruslantalpa/2eab8c930a65e8043d8f
sql = [q|
WITH synonyms AS (
/*
-- CTE to replace the view from information_schema because the information in it depended on the logged in role
-- notice the commented line
*/
WITH view_column_usage AS (
SELECT DISTINCT
CAST(current_database() AS character varying) AS view_catalog,
CAST(nv.nspname AS character varying) AS view_schema,
CAST(v.relname AS character varying) AS view_name,
CAST(current_database() AS character varying) AS table_catalog,
CAST(nt.nspname AS character varying) AS table_schema,
CAST(t.relname AS character varying) AS table_name,
CAST(a.attname AS character varying) AS column_name
FROM pg_namespace nv, pg_class v, pg_depend dv,
pg_depend dt, pg_class t, pg_namespace nt,
pg_attribute a
WHERE nv.oid = v.relnamespace
AND v.relkind = 'v'
AND v.oid = dv.refobjid
AND dv.refclassid = 'pg_catalog.pg_class'::regclass
AND dv.classid = 'pg_catalog.pg_rewrite'::regclass
AND dv.deptype = 'i'
AND dv.objid = dt.objid
AND dv.refobjid <> dt.refobjid
AND dt.classid = 'pg_catalog.pg_rewrite'::regclass
AND dt.refclassid = 'pg_catalog.pg_class'::regclass
AND dt.refobjid = t.oid
AND t.relnamespace = nt.oid
AND t.relkind IN ('r', 'v', 'f')
AND t.oid = a.attrelid
AND dt.refobjsubid = a.attnum
/*--AND pg_has_role(t.relowner, 'USAGE')*/
)
SELECT
vcu.table_schema AS src_table_schema,
vcu.table_name AS src_table_name,
vcu.column_name AS src_column_name,
view.schemaname AS syn_table_schema,
view.viewname AS syn_table_name,
view.definition AS view_definition
FROM
pg_catalog.pg_views AS view,
view_column_usage AS vcu
WHERE
view.schemaname = vcu.view_schema AND
view.viewname = vcu.view_name AND
view.schemaname NOT IN ('pg_catalog', 'information_schema')
/*--AND (SELECT COUNT(*) FROM information_schema.view_table_usage WHERE view_schema = view.schemaname AND view_name = view.viewname) = 1*/
WITH view_columns AS (
SELECT
c.oid AS view_oid,
a.attname::information_schema.sql_identifier AS column_name
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace nc ON c.relnamespace = nc.oid
WHERE
NOT pg_is_other_temp_schema(nc.oid)
AND a.attnum > 0
AND NOT a.attisdropped
AND (c.relkind = 'v'::"char")
AND nc.nspname NOT IN ('information_schema', 'pg_catalog')
),
view_column_usage AS (
SELECT DISTINCT
v.oid as view_oid,
nv.nspname::information_schema.sql_identifier AS view_schema,
v.relname::information_schema.sql_identifier AS view_name,
nt.nspname::information_schema.sql_identifier AS table_schema,
t.relname::information_schema.sql_identifier AS table_name,
a.attname::information_schema.sql_identifier AS column_name,
pg_get_viewdef(v.oid)::information_schema.character_data AS view_definition
FROM pg_namespace nv
JOIN pg_class v ON nv.oid = v.relnamespace
JOIN pg_depend dv ON v.oid = dv.refobjid
JOIN pg_depend dt ON dv.objid = dt.objid
JOIN pg_class t ON dt.refobjid = t.oid
JOIN pg_namespace nt ON t.relnamespace = nt.oid
JOIN pg_attribute a ON t.oid = a.attrelid AND dt.refobjsubid = a.attnum
WHERE
nv.nspname not in ('information_schema', 'pg_catalog')
AND v.relkind = 'v'::"char"
AND dv.refclassid = 'pg_class'::regclass::oid
AND dv.classid = 'pg_rewrite'::regclass::oid
AND dv.deptype = 'i'::"char"
AND dv.refobjid <> dt.refobjid
AND dt.classid = 'pg_rewrite'::regclass::oid
AND dt.refclassid = 'pg_class'::regclass::oid
AND (t.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
),
candidates AS (
SELECT
vcu.*,
(
SELECT CASE WHEN match IS NOT NULL THEN coalesce(match[7], match[4]) END
FROM REGEXP_MATCHES(
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
src_table_schema, src_table_name, src_column_name,
syn_table_schema, syn_table_name,
(regexp_matches(view_definition, CONCAT('\.(', src_column_name, ')(?=,|$)'), 'gn'))[1] AS syn_column_name
FROM synonyms
UNION (
SELECT
src_table_schema, src_table_name, src_column_name,
syn_table_schema, syn_table_name,
(regexp_matches(view_definition, CONCAT('\.', src_column_name, '\sAS\s("?)(.+?)\1(,|$)'), 'gn'))[2] AS syn_column_name /* " <- for syntax highlighting */
FROM synonyms
) |]
c.table_schema,
c.table_name,
c.column_name AS table_column_name,
c.view_schema,
c.view_name,
c.view_column_name
FROM view_columns AS vc, candidates AS c
WHERE
vc.view_oid = c.view_oid AND
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 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.Text (Text)
import qualified Data.Text as T
import qualified Hasql.Session as H
import qualified Hasql.Pool as P
import qualified Hasql.Session as H
import Network.HTTP.Types.Header
import qualified Network.HTTP.Types.Status as HT
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.HashMap.Strict as M
import Data.Maybe (fromMaybe)
import Data.Text
import Data.String.Conversions (cs)
import Data.Text
import Data.Time.Clock (NominalDiffTime)
import qualified Hasql.Transaction as H
import Network.HTTP.Types.Header (hAccept, hAuthorization)
import Network.HTTP.Types.Status (status415, status400)
import Network.Wai (Application, Request (..), Response,
requestHeaders)
import Network.HTTP.Types.Status (status400, status415)
import Network.Wai (Application, Request (..),
Response, requestHeaders)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.Static (only, staticPolicy)
+2 -2
View File
@@ -3,14 +3,14 @@ module PostgREST.Parsers
-- )
where
import Control.Applicative hiding ((<$>))
import Control.Applicative hiding ((<$>))
import Data.Monoid
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Tree
import PostgREST.QueryBuilder (operators)
import PostgREST.Types
import Text.ParserCombinators.Parsec hiding (many, (<|>))
import PostgREST.QueryBuilder (operators)
pRequestSelect :: Text -> Parser ReadRequest
pRequestSelect rootNodeName = do
+8 -8
View File
@@ -1,16 +1,16 @@
module PostgREST.Types where
import Data.Text
import Data.Tree
import qualified Data.ByteString.Lazy as BL
import Data.Aeson
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 Data.Aeson
import Data.Int (Int32)
data DbStructure = DbStructure {
dbTables :: [Table]
, dbColumns :: [Column]
, dbRelations :: [Relation]
dbTables :: [Table]
, dbColumns :: [Column]
, dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey]
} deriving (Show, Eq)
+1 -1
View File
@@ -9,7 +9,7 @@ extra-deps:
- postgresql-error-codes-1
- postgresql-binary-0.8.1
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:
- '.'
+56
View File
@@ -24,6 +24,7 @@ spec = do
, {"schema":"test","name":"comments","insertable":true}
, {"schema":"test","name":"complex_items","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":"has_count_column","insertable":false}
, {"schema":"test","name":"has_fk","insertable":true}
@@ -57,6 +58,61 @@ spec = do
{matchStatus = 200}
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" $
request methodOptions "/menagerie" [] "" `shouldRespondWith`
[json|
+1
View File
@@ -28,6 +28,7 @@ GRANT ALL ON TABLE
, projects_view
, simple_pk
, tasks
, filtered_tasks
, tsearch
, users
, users_projects
+19 -9
View File
@@ -594,6 +594,15 @@ CREATE TABLE simple_pk (
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: -
@@ -605,6 +614,16 @@ CREATE TABLE tasks (
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: -
@@ -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: -