Skip to content

Commit

Permalink
schema data model
Browse files Browse the repository at this point in the history
  • Loading branch information
olirice committed Sep 2, 2021
1 parent 908feb0 commit 8f0d898
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 18 deletions.
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def check_python_version():
"psycopg2",
"sqlalchemy",
"pre-commit",
"flupy",
"isort",
]


Expand Down
243 changes: 225 additions & 18 deletions sql/pg_graphql--0.1.sql
Original file line number Diff line number Diff line change
Expand Up @@ -318,27 +318,11 @@ $$;



grant all on schema gql to postgres;
grant all on all tables in schema gql to postgres;



















grant all on schema gql to postgres;
grant all on all tables in schema gql to postgres;



Expand All @@ -349,13 +333,236 @@ grant all on all tables in schema gql to postgres;



/*
create function gql.to_regclass(schema_ text, name_ text)
returns regclass
language sql
immutable
as
$$ select (quote_ident(schema_) || '.' || quote_ident(name_))::regclass; $$;
create function gql.to_table_name(regclass)
returns text
language sql
immutable
as
$$ select coalesce(nullif(split_part($1::text, '.', 2), ''), $1::text) $$;
create function gql.to_pascal_case(text)
returns text
language sql
immutable
as
$$
select
string_agg(initcap(part), '')
from
unnest(string_to_array($1, '_')) x(part)
$$;
-- Tables
create table gql.entity (
--id integer generated always as identity primary key,
entity regclass primary key,
is_disabled boolean default false
);
-- Populate gql.entity
insert into gql.entity(entity, is_disabled)
select
gql.to_regclass(schemaname, tablename) entity,
false is_disabled
from
pg_tables pgt
where
schemaname not in ('information_schema', 'pg_catalog', 'gql');
create type gql.type_type as enum('Scalar', 'Node', 'Edge', 'Connection', 'PageInfo', 'Object');
create table gql.type (
id integer generated always as identity primary key,
name text not null unique,
type_type gql.type_type not null,
entity regclass references gql.entity(entity),
is_disabled boolean not null default false,
-- Does the type need to be in the schema?
is_builtin boolean not null default false,
unique (type_type, entity)
);
-- Constants
insert into gql.type (name, type_type, is_builtin)
values
('ID', 'Scalar', true),
('Int', 'Scalar', true),
('Float', 'Scalar', true),
('String', 'Scalar', true),
('Boolean', 'Scalar', true),
('DateTime', 'Scalar', false),
('BigInt', 'Scalar', false),
('UUID', 'Scalar', false),
('JSON', 'Scalar', false),
('Query', 'Object', false),
('Mutation', 'Object', false),
('PageInfo', 'PageInfo', false);
-- Node Types
-- TODO snake case to camel case to handle underscores
insert into gql.type (name, type_type, entity, is_disabled, is_builtin)
select gql.to_pascal_case(gql.to_table_name(entity)), 'Node', entity, false, false
from gql.entity;
-- Edge Types
insert into gql.type (name, type_type, entity, is_disabled, is_builtin)
select gql.to_pascal_case(gql.to_table_name(entity)) || 'Edge', 'Edge', entity, false, false
from gql.entity;
-- Connection Types
insert into gql.type (name, type_type, entity, is_disabled, is_builtin)
select gql.to_pascal_case(gql.to_table_name(entity)) || 'Connection', 'Connection', entity, false, false
from gql.entity;
create function gql.type_id_by_name(text)
returns int
language sql
as
$$ select id from gql.type where name = $1; $$;
create table gql.field (
id integer generated always as identity primary key,
parent_type_id integer not null references gql.type(id),
type_id integer not null references gql.type(id),
name text not null,
is_not_null boolean,
is_array boolean default false,
is_array_not_null boolean,
-- TODO trigger check column name only non-null when type is scalar
column_name text,
-- Names must be unique on each type
unique(type_id, name),
-- is_array_not_null only set if is_array is true
check (
(not is_array and is_array_not_null is null)
or (is_array and is_array_not_null is not null)
)
);
-- PageInfo
insert into gql.field(parent_type_id, type_id, name, is_not_null, is_array, is_array_not_null, column_name)
values
(gql.type_id_by_name('PageInfo'), gql.type_id_by_name('Boolean'), 'hasPreviousPage', true, false, null, null),
(gql.type_id_by_name('PageInfo'), gql.type_id_by_name('Boolean'), 'hasNextPage', true, false, null, null),
(gql.type_id_by_name('PageInfo'), gql.type_id_by_name('String'), 'startCursor', true, false, null, null),
(gql.type_id_by_name('PageInfo'), gql.type_id_by_name('String'), 'endCursor', true, false, null, null);
-- Edges
insert into gql.field(parent_type_id, type_id, name, is_not_null, is_array, is_array_not_null, column_name)
-- Edge.node:
select
edge.id, node.id, 'node', false, false, null::boolean, null::text
from
gql.type edge
join gql.type node
on edge.entity = node.entity
where
edge.type_type = 'Edge'
and node.type_type = 'Node'
union all
-- Edge.cursor
select
edge.id, gql.type_id_by_name('String'), 'cursor', true, false, null, null
from
gql.type edge
where
edge.type_type = 'Edge';
-- Connection
insert into gql.field(parent_type_id, type_id, name, is_not_null, is_array, is_array_not_null, column_name)
-- Connection.edges:
select
conn.id, edge.id, 'edges', false, true, false::boolean, null::text
from
gql.type conn
join gql.type edge
on conn.entity = edge.entity
where
conn.type_type = 'Connection'
and edge.type_type = 'Edge'
union all
-- Connection.pageInfo
select
conn.id, gql.type_id_by_name('PageInfo'), 'pageInfo', true, false, null, null
from
gql.type conn
where
conn.type_type = 'Connection';
create function gql.sql_type_to_gql_type(sql_type text)
returns int
language sql
as
$$
-- SQL type from information_schema.columns.data_type
select
case
when sql_type like 'int%' then gql.type_id_by_name('Int')
when sql_type like 'bool%' then gql.type_id_by_name('Boolean')
when sql_type like 'float%' then gql.type_id_by_name('Float')
when sql_type like 'numeric%' then gql.type_id_by_name('Float')
when sql_type like 'json%' then gql.type_id_by_name('JSON')
when sql_type = 'uuid' then gql.type_id_by_name('UUID')
when sql_type like 'date%' then gql.type_id_by_name('DateTime')
when sql_type like 'timestamp%' then gql.type_id_by_name('DateTime')
else gql.type_id_by_name('String')
end;
$$;
-- Node
insert into gql.field(parent_type_id, type_id, name, is_not_null, is_array, is_array_not_null, column_name)
-- Node.<column>
select
gt.id,
c.data_type,
-- TODO check for pkey and int/bigint/uuid type => 'ID!'
case
-- substring removes the underscore prefix from array types
when c.data_type = 'ARRAY' then gql.sql_type_to_gql_type(substring(udt_name, 2, 100))
else gql.sql_type_to_gql_type(c.data_type)
end,
ent.entity,
gt.name,
gt.id,
c.*
from
gql.entity ent
join gql.type gt
on ent.entity = gt.entity
join information_schema.role_column_grants rcg
on ent.entity = gql.to_regclass(rcg.table_schema, rcg.table_name)
join information_schema.columns c
on rcg.table_schema = c.table_schema
and rcg.table_name = c.table_name
and rcg.column_name = c.column_name,
left join pg_index pgi
on ent.entity = pgi.oid
, pg_class, pg_attribute, pg_namespace
where
gt.type_type = 'Node'
-- INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER
and rcg.privilege_type = 'SELECT'
and (
-- Use access level of current role
rcg.grantee = current_setting('role')
-- If superuser, allow everything
or current_setting('role') = 'none'
)
order by
ent.entity, c.ordinal_position;
*/

9 changes: 9 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import json
import os
import subprocess
from pathlib import Path
from flupy import walk_files
import time

import pytest
Expand Down Expand Up @@ -69,6 +71,13 @@ def dockerize_database():
@pytest.fixture(scope="session")
def engine(dockerize_database):
eng = create_engine(f"postgresql://postgres:password@localhost:{PORT}/{DB_NAME}")

path = Path('test/setup.sql')
contents = path.read_text()
with eng.connect() as conn:
conn.execute(text(contents))
conn.execute(text("commit"))

eng.execute(
text(
"""
Expand Down
9 changes: 9 additions & 0 deletions test/setup.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
create extension "uuid-ossp";

create table account(
id uuid not null default uuid_generate_v4() primary key,
email varchar(255) not null,
encrypted_password varchar(255) not null,
created_at timestamp not null,
updated_at timestamp not null
);
63 changes: 63 additions & 0 deletions test/test_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import json

from sqlalchemy import func, select, text


def test_get_name(sess):

selection = json.dumps({
"kind": "Field",
"name": {
"kind": "Name",
"value": "hello"
},
"alias": None,
"arguments": None,
"directives": None,
"selectionSet": None
})

(result,) = sess.execute(select([func.gql.get_name(selection)])).fetchone()

assert result == "hello"


def test_get_alias(sess):

selection = json.dumps({
"kind": "Field",
"name": {
"kind": "Name",
"value": "hello"
},
"alias": {
"kind": "Name",
"value": "hello_alias"
},
"arguments": None,
"directives": None,
"selectionSet": None
})

(result,) = sess.execute(select([func.gql.get_alias(selection)])).fetchone()

assert result == "hello_alias"


def test_get_alias_defaults_to_name(sess):

selection = json.dumps({
"kind": "Field",
"name": {
"kind": "Name",
"value": "hello"
},
"alias": None,
"arguments": None,
"directives": None,
"selectionSet": None
})

(result,) = sess.execute(select([func.gql.get_alias(selection)])).fetchone()

assert result == "hello"
Loading

0 comments on commit 8f0d898

Please sign in to comment.