Merge @ruslantalpa's fk improved detection
This commit is contained in:
+3
-2
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -10,23 +10,25 @@ module PostgREST.DbStructure (
|
|||||||
, doesProcReturnJWT
|
, doesProcReturnJWT
|
||||||
) where
|
) 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.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 (
|
||||||
/*
|
|
||||||
-- 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
|
SELECT
|
||||||
vcu.table_schema AS src_table_schema,
|
c.oid AS view_oid,
|
||||||
vcu.table_name AS src_table_name,
|
a.attname::information_schema.sql_identifier AS column_name
|
||||||
vcu.column_name AS src_column_name,
|
FROM pg_attribute a
|
||||||
view.schemaname AS syn_table_schema,
|
JOIN pg_class c ON a.attrelid = c.oid
|
||||||
view.viewname AS syn_table_name,
|
JOIN pg_namespace nc ON c.relnamespace = nc.oid
|
||||||
view.definition AS view_definition
|
|
||||||
FROM
|
|
||||||
pg_catalog.pg_views AS view,
|
|
||||||
view_column_usage AS vcu
|
|
||||||
WHERE
|
WHERE
|
||||||
view.schemaname = vcu.view_schema AND
|
NOT pg_is_other_temp_schema(nc.oid)
|
||||||
view.viewname = vcu.view_name AND
|
AND a.attnum > 0
|
||||||
view.schemaname NOT IN ('pg_catalog', 'information_schema')
|
AND NOT a.attisdropped
|
||||||
/*--AND (SELECT COUNT(*) FROM information_schema.view_table_usage WHERE view_schema = view.schemaname AND view_name = view.viewname) = 1*/
|
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
|
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ 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
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
module PostgREST.Types where
|
module PostgREST.Types where
|
||||||
|
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.Text
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
import qualified Data.ByteString.Lazy as BL
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
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]
|
||||||
|
|||||||
+1
-1
@@ -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:
|
||||||
- '.'
|
- '.'
|
||||||
|
|||||||
@@ -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|
|
||||||
|
|||||||
Vendored
+1
@@ -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
|
||||||
|
|||||||
Vendored
+19
-9
@@ -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: -
|
||||||
|
|||||||
Reference in New Issue
Block a user