Compare commits

..

No commits in common. "master" and "v0.14.6" have entirely different histories.

57 changed files with 947 additions and 1899 deletions

View File

@ -12,7 +12,8 @@ FROM golang:1.14-alpine as go-build
RUN apk update && \
apk add --no-cache make && \
apk add --no-cache git && \
apk add --no-cache jq
apk add --no-cache jq && \
apk add --no-cache upx=3.95-r2
RUN GO111MODULE=off go get -u github.com/rafaelsq/wtc

View File

@ -88,8 +88,9 @@ type SuperGraph struct {
queries map[uint64]*query
roles map[string]*Role
getRole *sql.Stmt
rmap map[uint64]resolvFn
rmap map[uint64]*resolvFn
abacEnabled bool
anonExists bool
qc *qcode.Compiler
pc *psql.Compiler
ge *graphql.Engine
@ -139,7 +140,7 @@ func newSuperGraph(conf *Config, db *sql.DB, dbinfo *psql.DBInfo) (*SuperGraph,
return nil, err
}
if conf.SecretKey != "" {
if len(conf.SecretKey) != 0 {
sk := sha256.Sum256([]byte(conf.SecretKey))
conf.SecretKey = ""
sg.encKey = sk

View File

@ -12,8 +12,7 @@ import (
// to a prepared statement.
func (c *scontext) argList(md psql.Metadata) ([]interface{}, error) {
params := md.Params()
vars := make([]interface{}, len(params))
vars := make([]interface{}, len(md.Params))
var fields map[string]json.RawMessage
var err error
@ -26,7 +25,7 @@ func (c *scontext) argList(md psql.Metadata) ([]interface{}, error) {
}
}
for i, p := range params {
for i, p := range md.Params {
switch p.Name {
case "user_id":
if v := c.Value(UserIDKey); v != nil {

41
core/bench.11 Normal file
View File

@ -0,0 +1,41 @@
INF roles_query not defined: attribute based access control disabled
all expectations were already fulfilled, call to Query 'SELECT jsonb_build_object('users', "__sj_0"."json", 'products', "__sj_1"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "__sj_2"."json" AS "customers", "__sj_3"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_1" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "users_3"."full_name" AS "full_name", "users_3"."phone" AS "phone", "users_3"."email" AS "email" FROM (SELECT "users"."full_name", "users"."phone", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_1"."user_id"))) LIMIT ('1') :: integer) AS "users_3") AS "__sr_3") AS "__sj_3" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_1"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true')) AS "__sr_1") AS "__sj_1") AS "__sj_1", (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."name" AS "name" FROM (SELECT "users"."id" FROM "users" GROUP BY "users"."id" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"' with args [] was not expected
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/core
BenchmarkGraphQL-16 INF roles_query not defined: attribute based access control disabled
all expectations were already fulfilled, call to Query 'SELECT jsonb_build_object('users', "__sj_0"."json", 'products', "__sj_1"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "__sj_2"."json" AS "customers", "__sj_3"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_1" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "users_3"."full_name" AS "full_name", "users_3"."phone" AS "phone", "users_3"."email" AS "email" FROM (SELECT "users"."full_name", "users"."phone", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_1"."user_id"))) LIMIT ('1') :: integer) AS "users_3") AS "__sr_3") AS "__sj_3" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_1"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true')) AS "__sr_1") AS "__sj_1") AS "__sj_1", (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."name" AS "name" FROM (SELECT "users"."id" FROM "users" GROUP BY "users"."id" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"' with args [] was not expected
INF roles_query not defined: attribute based access control disabled
all expectations were already fulfilled, call to Query 'SELECT jsonb_build_object('users', "__sj_0"."json", 'products', "__sj_1"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "__sj_2"."json" AS "customers", "__sj_3"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_1" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "users_3"."full_name" AS "full_name", "users_3"."phone" AS "phone", "users_3"."email" AS "email" FROM (SELECT "users"."full_name", "users"."phone", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_1"."user_id"))) LIMIT ('1') :: integer) AS "users_3") AS "__sr_3") AS "__sj_3" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_1"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true')) AS "__sr_1") AS "__sj_1") AS "__sj_1", (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."name" AS "name" FROM (SELECT "users"."id" FROM "users" GROUP BY "users"."id" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"' with args [] was not expected
INF roles_query not defined: attribute based access control disabled
all expectations were already fulfilled, call to Query 'SELECT jsonb_build_object('users', "__sj_0"."json", 'products', "__sj_1"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "__sj_2"."json" AS "customers", "__sj_3"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_1" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "users_3"."full_name" AS "full_name", "users_3"."phone" AS "phone", "users_3"."email" AS "email" FROM (SELECT "users"."full_name", "users"."phone", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_1"."user_id"))) LIMIT ('1') :: integer) AS "users_3") AS "__sr_3") AS "__sj_3" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_1"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true')) AS "__sr_1") AS "__sj_1") AS "__sj_1", (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."name" AS "name" FROM (SELECT "users"."id" FROM "users" GROUP BY "users"."id" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"' with args [] was not expected
105048 10398 ns/op 18342 B/op 55 allocs/op
PASS
ok github.com/dosco/super-graph/core 1.328s
PASS
ok github.com/dosco/super-graph/core/internal/allow 0.088s
? github.com/dosco/super-graph/core/internal/crypto [no test files]
? github.com/dosco/super-graph/core/internal/integration_tests [no test files]
PASS
ok github.com/dosco/super-graph/core/internal/integration_tests/cockroachdb 0.121s
PASS
ok github.com/dosco/super-graph/core/internal/integration_tests/postgresql 0.118s
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/core/internal/psql
BenchmarkCompile-16 79845 14428 ns/op 4584 B/op 39 allocs/op
BenchmarkCompileParallel-16 326205 3918 ns/op 4633 B/op 39 allocs/op
PASS
ok github.com/dosco/super-graph/core/internal/psql 2.696s
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/core/internal/qcode
BenchmarkQCompile-16 146953 8049 ns/op 3756 B/op 28 allocs/op
BenchmarkQCompileP-16 475936 2447 ns/op 3790 B/op 28 allocs/op
BenchmarkParse-16 140811 8163 ns/op 3902 B/op 18 allocs/op
BenchmarkParseP-16 571345 2041 ns/op 3903 B/op 18 allocs/op
BenchmarkSchemaParse-16 230715 5012 ns/op 3968 B/op 57 allocs/op
BenchmarkSchemaParseP-16 802426 1565 ns/op 3968 B/op 57 allocs/op
PASS
ok github.com/dosco/super-graph/core/internal/qcode 8.427s
? github.com/dosco/super-graph/core/internal/util [no test files]

View File

@ -82,13 +82,12 @@ func (sg *SuperGraph) buildMultiStmt(query, vars []byte) ([]stmt, error) {
}
}
if sg.conf.RolesQuery == "" {
if len(sg.conf.RolesQuery) == 0 {
return nil, errors.New("roles_query not defined")
}
stmts := make([]stmt, 0, len(sg.conf.Roles))
w := &bytes.Buffer{}
md := psql.Metadata{}
for i := 0; i < len(sg.conf.Roles); i++ {
role := &sg.conf.Roles[i]
@ -106,18 +105,16 @@ func (sg *SuperGraph) buildMultiStmt(query, vars []byte) ([]stmt, error) {
stmts = append(stmts, stmt{role: role, qc: qc})
s := &stmts[len(stmts)-1]
md, err = sg.pc.CompileWithMetadata(w, qc, psql.Variables(vm), md)
s.md, err = sg.pc.Compile(w, qc, psql.Variables(vm))
if err != nil {
return nil, err
}
s.sql = w.String()
s.md = md
w.Reset()
}
sql, err := sg.renderUserQuery(md, stmts)
sql, err := sg.renderUserQuery(stmts)
if err != nil {
return nil, err
}
@ -127,13 +124,13 @@ func (sg *SuperGraph) buildMultiStmt(query, vars []byte) ([]stmt, error) {
}
//nolint: errcheck
func (sg *SuperGraph) renderUserQuery(md psql.Metadata, stmts []stmt) (string, error) {
func (sg *SuperGraph) renderUserQuery(stmts []stmt) (string, error) {
w := &bytes.Buffer{}
io.WriteString(w, `SELECT "_sg_auth_info"."role", (CASE "_sg_auth_info"."role" `)
for _, s := range stmts {
if s.role.Match == "" &&
if len(s.role.Match) == 0 &&
s.role.Name != "user" && s.role.Name != "anon" {
continue
}
@ -145,12 +142,12 @@ func (sg *SuperGraph) renderUserQuery(md psql.Metadata, stmts []stmt) (string, e
}
io.WriteString(w, `END) FROM (SELECT (CASE WHEN EXISTS (`)
md.RenderVar(w, sg.conf.RolesQuery)
io.WriteString(w, sg.conf.RolesQuery)
io.WriteString(w, `) THEN `)
io.WriteString(w, `(SELECT (CASE`)
for _, s := range stmts {
if s.role.Match == "" {
if len(s.role.Match) == 0 {
continue
}
io.WriteString(w, ` WHEN `)
@ -161,7 +158,7 @@ func (sg *SuperGraph) renderUserQuery(md psql.Metadata, stmts []stmt) (string, e
}
io.WriteString(w, ` ELSE 'user' END) FROM (`)
md.RenderVar(w, sg.conf.RolesQuery)
io.WriteString(w, sg.conf.RolesQuery)
io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) `)
io.WriteString(w, `ELSE 'anon' END) FROM (VALUES (1)) AS "_sg_auth_filler") AS "_sg_auth_info"(role) LIMIT 1; `)

View File

@ -72,7 +72,6 @@ type Config struct {
type Table struct {
Name string
Table string
Type string
Blocklist []string
Remotes []Remote
Columns []Column
@ -152,7 +151,7 @@ type Delete struct {
// AddRoleTable function is a helper function to make it easy to add per-table
// row-level config
func (c *Config) AddRoleTable(role, table string, conf interface{}) error {
func (c *Config) AddRoleTable(role string, table string, conf interface{}) error {
var r *Role
for i := range c.Roles {
@ -198,26 +197,30 @@ func (c *Config) AddRoleTable(role, table string, conf interface{}) error {
// ReadInConfig function reads in the config file for the environment specified in the GO_ENV
// environment variable. This is the best way to create a new Super Graph config.
func ReadInConfig(configFile string) (*Config, error) {
cp := path.Dir(configFile)
vi := newViper(cp, path.Base(configFile))
cpath := path.Dir(configFile)
cfile := path.Base(configFile)
vi := newViper(cpath, cfile)
if err := vi.ReadInConfig(); err != nil {
return nil, err
}
if pcf := vi.GetString("inherits"); pcf != "" {
cf := vi.ConfigFileUsed()
vi = newViper(cp, pcf)
inherits := vi.GetString("inherits")
if inherits != "" {
vi = newViper(cpath, inherits)
if err := vi.ReadInConfig(); err != nil {
return nil, err
}
if v := vi.GetString("inherits"); v != "" {
return nil, fmt.Errorf("inherited config (%s) cannot itself inherit (%s)", pcf, v)
if vi.IsSet("inherits") {
return nil, fmt.Errorf("inherited config (%s) cannot itself inherit (%s)",
inherits,
vi.GetString("inherits"))
}
vi.SetConfigFile(cf)
vi.SetConfigName(cfile)
if err := vi.MergeInConfig(); err != nil {
return nil, err
@ -231,7 +234,7 @@ func ReadInConfig(configFile string) (*Config, error) {
}
if c.AllowListFile == "" {
c.AllowListFile = path.Join(cp, "allow.list")
c.AllowListFile = path.Join(cpath, "allow.list")
}
return c, nil
@ -245,7 +248,7 @@ func newViper(configPath, configFile string) *viper.Viper {
vi.AutomaticEnv()
if filepath.Ext(configFile) != "" {
vi.SetConfigFile(path.Join(configPath, configFile))
vi.SetConfigFile(configFile)
} else {
vi.SetConfigName(configFile)
vi.AddConfigPath(configPath)

View File

@ -125,7 +125,7 @@ func (c *scontext) execQuery() ([]byte, error) {
return nil, err
}
if len(data) == 0 || st.md.Skipped() == 0 {
if len(data) == 0 || st.md.Skipped == 0 {
return data, nil
}
@ -172,9 +172,8 @@ func (c *scontext) resolvePreparedSQL() ([]byte, *stmt, error) {
h := maphash.Hash{}
h.SetSeed(c.sg.hashSeed)
id := queryID(&h, c.res.name, role)
q, ok := c.sg.queries[id]
q, ok := c.sg.queries[queryID(&h, c.res.name, role)]
if !ok {
return nil, nil, errNotFound
}
@ -305,7 +304,7 @@ func (c *scontext) resolveSQL() ([]byte, *stmt, error) {
err = row.Scan(&root)
}
if role == "" {
if len(role) == 0 {
c.res.role = c.role
} else {
c.res.role = role

View File

@ -21,7 +21,7 @@ func (sg *SuperGraph) initConfig() error {
for i := 0; i < len(c.Tables); i++ {
t := &c.Tables[i]
// t.Name = flect.Pluralize(strings.ToLower(t.Name))
t.Name = flect.Pluralize(strings.ToLower(t.Name))
if _, ok := tm[t.Name]; ok {
sg.conf.Tables = append(c.Tables[:i], c.Tables[i+1:]...)
@ -75,22 +75,13 @@ func (sg *SuperGraph) initConfig() error {
if c.RolesQuery == "" {
sg.log.Printf("INF roles_query not defined: attribute based access control disabled")
} else {
n := 0
for k, v := range sg.roles {
if k == "user" || k == "anon" {
n++
} else if v.Match != "" {
n++
}
}
sg.abacEnabled = (n > 2)
if !sg.abacEnabled {
sg.log.Printf("WRN attribute based access control disabled: no custom roles found (with 'match' defined)")
}
}
_, userExists := sg.roles["user"]
_, sg.anonExists = sg.roles["anon"]
sg.abacEnabled = userExists && c.RolesQuery != ""
return nil
}
@ -100,26 +91,21 @@ func getDBTableAliases(c *Config) map[string][]string {
for i := range c.Tables {
t := c.Tables[i]
if t.Table != "" && t.Type == "" {
m[t.Table] = append(m[t.Table], t.Name)
if len(t.Table) == 0 || len(t.Columns) != 0 {
continue
}
m[t.Table] = append(m[t.Table], t.Name)
}
return m
}
func addTables(c *Config, di *psql.DBInfo) error {
var err error
for _, t := range c.Tables {
switch t.Type {
case "json", "jsonb":
err = addJsonTable(di, t.Columns, t)
case "polymorphic":
err = addVirtualTable(di, t.Columns, t)
if t.Table == "" || len(t.Columns) == 0 {
continue
}
if err != nil {
if err := addTable(di, t.Columns, t); err != nil {
return err
}
@ -127,18 +113,17 @@ func addTables(c *Config, di *psql.DBInfo) error {
return nil
}
func addJsonTable(di *psql.DBInfo, cols []Column, t Table) error {
// This is for jsonb columns that want to be tables.
func addTable(di *psql.DBInfo, cols []Column, t Table) error {
bc, ok := di.GetColumn(t.Table, t.Name)
if !ok {
return fmt.Errorf(
"json table: column '%s' not found on table '%s'",
"Column '%s' not found on table '%s'",
t.Name, t.Table)
}
if bc.Type != "json" && bc.Type != "jsonb" {
return fmt.Errorf(
"json table: column '%s' in table '%s' is of type '%s'. Only JSON or JSONB is valid",
"Column '%s' in table '%s' is of type '%s'. Only JSON or JSONB is valid",
t.Name, t.Table, bc.Type)
}
@ -165,38 +150,8 @@ func addJsonTable(di *psql.DBInfo, cols []Column, t Table) error {
return nil
}
func addVirtualTable(di *psql.DBInfo, cols []Column, t Table) error {
if len(cols) == 0 {
return fmt.Errorf("polymorphic table: no id column specified")
}
c := cols[0]
if c.ForeignKey == "" {
return fmt.Errorf("polymorphic table: no 'related_to' specified on id column")
}
s := strings.SplitN(c.ForeignKey, ".", 2)
if len(s) != 2 {
return fmt.Errorf("polymorphic table: foreign key must be <type column>.<foreign key column>")
}
di.VTables = append(di.VTables, psql.VirtualTable{
Name: t.Name,
IDColumn: c.Name,
TypeColumn: s[0],
FKeyColumn: s[1],
})
return nil
}
func addForeignKeys(c *Config, di *psql.DBInfo) error {
for _, t := range c.Tables {
if t.Type != "" {
continue
}
for _, c := range t.Columns {
if c.ForeignKey == "" {
continue
@ -210,52 +165,30 @@ func addForeignKeys(c *Config, di *psql.DBInfo) error {
}
func addForeignKey(di *psql.DBInfo, c Column, t Table) error {
var tn string
if t.Type == "polymorphic" {
tn = t.Table
} else {
tn = t.Name
}
c1, ok := di.GetColumn(tn, c.Name)
c1, ok := di.GetColumn(t.Name, c.Name)
if !ok {
return fmt.Errorf(
"config: invalid table '%s' or column '%s' defined",
tn, c.Name)
"Invalid table '%s' or column '%s' in Config",
t.Name, c.Name)
}
v := strings.SplitN(c.ForeignKey, ".", 2)
if len(v) != 2 {
return fmt.Errorf(
"config: invalid foreign_key defined for table '%s' and column '%s': %s",
tn, c.Name, c.ForeignKey)
}
// check if it's a polymorphic foreign key
if _, ok := di.GetColumn(tn, v[0]); ok {
c2, ok := di.GetColumn(tn, v[1])
if !ok {
return fmt.Errorf(
"config: invalid column '%s' for polymorphic relationship on table '%s' and column '%s'",
v[1], tn, c.Name)
}
c1.FKeyTable = v[0]
c1.FKeyColID = []int16{c2.ID}
return nil
"Invalid foreign_key in Config for table '%s' and column '%s",
t.Name, c.Name)
}
fkt, fkc := v[0], v[1]
c3, ok := di.GetColumn(fkt, fkc)
c2, ok := di.GetColumn(fkt, fkc)
if !ok {
return fmt.Errorf(
"config: foreign_key for table '%s' and column '%s' points to unknown table '%s' and column '%s'",
t.Name, c.Name, v[0], v[1])
"Invalid foreign_key in Config for table '%s' and column '%s",
t.Name, c.Name)
}
c1.FKeyTable = fkt
c1.FKeyColID = []int16{c3.ID}
c1.FKeyColID = []int16{c2.ID}
return nil
}

View File

@ -10,23 +10,21 @@ import (
"os"
"sort"
"strings"
"text/scanner"
"github.com/chirino/graphql/schema"
"github.com/dosco/super-graph/jsn"
)
const (
expComment = iota + 1
expVar
expQuery
AL_QUERY int = iota + 1
AL_VARS
)
type Item struct {
Name string
key string
Query string
Vars string
Vars json.RawMessage
Comment string
}
@ -128,101 +126,121 @@ func (al *List) Set(vars []byte, query, comment string) error {
return errors.New("empty query")
}
var q string
for i := 0; i < len(query); i++ {
c := query[i]
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' {
q = query
break
} else if c == '{' {
q = "query " + query
break
}
}
al.saveChan <- Item{
Comment: comment,
Query: query,
Vars: string(vars),
Query: q,
Vars: vars,
}
return nil
}
func (al *List) Load() ([]Item, error) {
var list []Item
varString := "variables"
b, err := ioutil.ReadFile(al.filepath)
if err != nil {
return nil, err
return list, err
}
return parse(string(b), al.filepath)
}
if len(b) == 0 {
return list, nil
}
func parse(b, filename string) ([]Item, error) {
var items []Item
var comment bytes.Buffer
var varBytes []byte
var s scanner.Scanner
s.Init(strings.NewReader(b))
s.Filename = filename
s.Mode ^= scanner.SkipComments
itemMap := make(map[string]struct{})
var op, sp scanner.Position
var item Item
s, e, c := 0, 0, 0
ty := 0
newComment := false
st := expComment
for {
fq := false
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
txt := s.TokenText()
switch {
case strings.HasPrefix(txt, "/*"):
if st == expQuery {
v := b[sp.Offset:s.Pos().Offset]
item.Query = strings.TrimSpace(v[:strings.LastIndexByte(v, '}')+1])
items = append(items, item)
if c == 0 && b[e] == '#' {
s = e
for e < len(b) && b[e] != '\n' {
e++
}
item = Item{Comment: strings.TrimSpace(txt[2 : len(txt)-2])}
sp = s.Pos()
st = expComment
newComment = true
case !newComment && strings.HasPrefix(txt, "#"):
if st == expQuery {
v := b[sp.Offset:s.Pos().Offset]
item.Query = strings.TrimSpace(v[:strings.LastIndexByte(v, '}')+1])
items = append(items, item)
if (e - s) > 2 {
comment.Write(b[(s + 1):(e + 1)])
}
item = Item{}
sp = s.Pos()
st = expComment
}
case strings.HasPrefix(txt, "variables"):
if st == expComment {
v := b[sp.Offset:s.Pos().Offset]
item.Comment = strings.TrimSpace(v[:strings.IndexByte(v, '\n')])
}
sp = s.Pos()
st = expVar
if e >= len(b) {
break
}
case isGraphQL(txt):
if st == expVar {
v := b[sp.Offset:s.Pos().Offset]
item.Vars = strings.TrimSpace(v[:strings.LastIndexByte(v, '}')+1])
if matchPrefix(b, e, "query") || matchPrefix(b, e, "mutation") {
if c == 0 {
s = e
}
sp = op
st = expQuery
ty = AL_QUERY
} else if matchPrefix(b, e, varString) {
if c == 0 {
s = e + len(varString) + 1
}
ty = AL_VARS
} else if b[e] == '{' {
c++
} else if b[e] == '}' {
c--
if c == 0 {
if ty == AL_QUERY {
fq = true
} else if ty == AL_VARS {
varBytes = b[s:(e + 1)]
}
ty = 0
}
}
if fq {
query := string(b[s:(e + 1)])
name := QueryName(query)
key := strings.ToLower(name)
if _, ok := itemMap[key]; !ok {
v := Item{
Name: name,
key: key,
Query: query,
Vars: varBytes,
Comment: comment.String(),
}
list = append(list, v)
comment.Reset()
}
varBytes = nil
}
op = s.Pos()
e++
if e >= len(b) {
break
}
}
if st == expQuery {
v := b[sp.Offset:s.Pos().Offset]
item.Query = strings.TrimSpace(v[:strings.LastIndexByte(v, '}')+1])
items = append(items, item)
}
for i := range items {
items[i].Name = QueryName(items[i].Query)
items[i].key = strings.ToLower(items[i].Name)
}
return items, nil
}
func isGraphQL(s string) bool {
return strings.HasPrefix(s, "query") ||
strings.HasPrefix(s, "mutation") ||
strings.HasPrefix(s, "subscription")
return list, nil
}
func (al *List) save(item Item) error {
@ -279,43 +297,57 @@ func (al *List) save(item Item) error {
return strings.Compare(list[i].key, list[j].key) == -1
})
for i, v := range list {
var vars string
if v.Vars != "" {
buf.Reset()
if err := jsn.Clear(&buf, []byte(v.Vars)); err != nil {
for _, v := range list {
cmtLines := strings.Split(v.Comment, "\n")
i := 0
for _, c := range cmtLines {
if c = strings.TrimSpace(c); c == "" {
continue
}
_, err := f.WriteString(fmt.Sprintf("# %s\n", c))
if err != nil {
return err
}
i++
}
if i != 0 {
if _, err := f.WriteString("\n"); err != nil {
return err
}
} else {
if _, err := f.WriteString(fmt.Sprintf("# Query named %s\n\n", v.Name)); err != nil {
return err
}
}
if len(v.Vars) != 0 && !bytes.Equal(v.Vars, []byte("{}")) {
buf.Reset()
if err := jsn.Clear(&buf, v.Vars); err != nil {
return fmt.Errorf("failed to clean vars: %w", err)
}
vj := json.RawMessage(buf.Bytes())
if vj, err = json.MarshalIndent(vj, "", " "); err != nil {
continue
vj, err = json.MarshalIndent(vj, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal vars: %w", err)
}
vars = string(vj)
}
list[i].Vars = vars
list[i].Comment = strings.TrimSpace(v.Comment)
}
for _, v := range list {
if v.Comment != "" {
_, err = f.WriteString(fmt.Sprintf("/* %s */\n\n", v.Comment))
} else {
_, err = f.WriteString(fmt.Sprintf("/* %s */\n\n", v.Name))
}
if err != nil {
return err
}
if v.Vars != "" {
_, err = f.WriteString(fmt.Sprintf("variables %s\n\n", v.Vars))
_, err = f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
if err != nil {
return err
}
}
_, err = f.WriteString(fmt.Sprintf("%s\n\n", v.Query))
if v.Query[0] == '{' {
_, err = f.WriteString(fmt.Sprintf("query %s\n\n", v.Query))
} else {
_, err = f.WriteString(fmt.Sprintf("%s\n\n", v.Query))
}
if err != nil {
return err
}
@ -324,6 +356,18 @@ func (al *List) save(item Item) error {
return nil
}
func matchPrefix(b []byte, i int, s string) bool {
if (len(b) - i) < len(s) {
return false
}
for n := 0; n < len(s); n++ {
if b[(i+n)] != s[n] {
return false
}
}
return true
}
func QueryName(b string) string {
state, s := 0, 0

View File

@ -14,7 +14,7 @@ func TestGQLName1(t *testing.T) {
name := QueryName(q)
if name != "" {
if len(name) != 0 {
t.Fatal("Name should be empty, not ", name)
}
}
@ -82,160 +82,3 @@ func TestGQLName5(t *testing.T) {
t.Fatal("Name should be empty, not ", name)
}
}
func TestParse1(t *testing.T) {
var al = `
# Hello world
variables {
"data": {
"slug": "",
"body": "",
"post": {
"connect": {
"slug": ""
}
}
}
}
mutation createComment {
comment(insert: $data) {
slug
body
createdAt: created_at
totalVotes: cached_votes_total
totalReplies: cached_replies_total
vote: comment_vote(where: {user_id: {eq: $user_id}}) {
created_at
__typename
}
author: user {
slug
firstName: first_name
lastName: last_name
pictureURL: picture_url
bio
__typename
}
__typename
}
}
# Query named createPost
query createPost {
post(insert: $data) {
slug
body
published
createdAt: created_at
totalVotes: cached_votes_total
totalComments: cached_comments_total
vote: post_vote(where: {user_id: {eq: $user_id}}) {
created_at
__typename
}
author: user {
slug
firstName: first_name
lastName: last_name
pictureURL: picture_url
bio
__typename
}
__typename
}
}`
_, err := parse(al, "allow.list")
if err != nil {
t.Fatal(err)
}
}
func TestParse2(t *testing.T) {
var al = `
/* Hello world */
variables {
"data": {
"slug": "",
"body": "",
"post": {
"connect": {
"slug": ""
}
}
}
}
mutation createComment {
comment(insert: $data) {
slug
body
createdAt: created_at
totalVotes: cached_votes_total
totalReplies: cached_replies_total
vote: comment_vote(where: {user_id: {eq: $user_id}}) {
created_at
__typename
}
author: user {
slug
firstName: first_name
lastName: last_name
pictureURL: picture_url
bio
__typename
}
__typename
}
}
/*
Query named createPost
*/
variables {
"data": {
"thread": {
"connect": {
"slug": ""
}
},
"slug": "",
"published": false,
"body": ""
}
}
query createPost {
post(insert: $data) {
slug
body
published
createdAt: created_at
totalVotes: cached_votes_total
totalComments: cached_comments_total
vote: post_vote(where: {user_id: {eq: $user_id}}) {
created_at
__typename
}
author: user {
slug
firstName: first_name
lastName: last_name
pictureURL: picture_url
bio
__typename
}
__typename
}
}`
_, err := parse(al, "allow.list")
if err != nil {
t.Fatal(err)
}
}

View File

@ -1,3 +1,4 @@
//nolint:errcheck
package psql
import (
@ -111,15 +112,15 @@ func (c *compilerContext) renderColumnSearchRank(sel *qcode.Select, ti *DBTableI
c.renderComma(columnsRendered)
//fmt.Fprintf(w, `ts_rank("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
//c.sel.Name, cn, arg.Val, col.Name)
_, _ = io.WriteString(c.w, `ts_rank(`)
io.WriteString(c.w, `ts_rank(`)
colWithTable(c.w, ti.Name, cn)
if c.schema.ver >= 110000 {
_, _ = io.WriteString(c.w, `, websearch_to_tsquery(`)
io.WriteString(c.w, `, websearch_to_tsquery(`)
} else {
_, _ = io.WriteString(c.w, `, to_tsquery(`)
io.WriteString(c.w, `, to_tsquery(`)
}
c.md.renderValueExp(c.w, Param{Name: arg.Val, Type: "string"})
_, _ = io.WriteString(c.w, `))`)
c.renderValueExp(Param{Name: arg.Val, Type: "string"})
io.WriteString(c.w, `))`)
alias(c.w, col.Name)
return nil
@ -136,15 +137,15 @@ func (c *compilerContext) renderColumnSearchHeadline(sel *qcode.Select, ti *DBTa
c.renderComma(columnsRendered)
//fmt.Fprintf(w, `ts_headline("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
//c.sel.Name, cn, arg.Val, col.Name)
_, _ = io.WriteString(c.w, `ts_headline(`)
io.WriteString(c.w, `ts_headline(`)
colWithTable(c.w, ti.Name, cn)
if c.schema.ver >= 110000 {
_, _ = io.WriteString(c.w, `, websearch_to_tsquery(`)
io.WriteString(c.w, `, websearch_to_tsquery(`)
} else {
_, _ = io.WriteString(c.w, `, to_tsquery(`)
io.WriteString(c.w, `, to_tsquery(`)
}
c.md.renderValueExp(c.w, Param{Name: arg.Val, Type: "string"})
_, _ = io.WriteString(c.w, `))`)
c.renderValueExp(Param{Name: arg.Val, Type: "string"})
io.WriteString(c.w, `))`)
alias(c.w, col.Name)
return nil
@ -156,9 +157,9 @@ func (c *compilerContext) renderColumnTypename(sel *qcode.Select, ti *DBTableInf
}
c.renderComma(columnsRendered)
_, _ = io.WriteString(c.w, `(`)
io.WriteString(c.w, `(`)
squoted(c.w, ti.Name)
_, _ = io.WriteString(c.w, ` :: text)`)
io.WriteString(c.w, ` :: text)`)
alias(c.w, col.Name)
return nil
@ -168,9 +169,9 @@ func (c *compilerContext) renderColumnFunction(sel *qcode.Select, ti *DBTableInf
pl := funcPrefixLen(c.schema.fm, col.Name)
// if pl == 0 {
// //fmt.Fprintf(w, `'%s not defined' AS %s`, cn, col.Name)
// _, _ = io.WriteString(c.w, `'`)
// _, _ = io.WriteString(c.w, col.Name)
// _, _ = io.WriteString(c.w, ` not defined'`)
// io.WriteString(c.w, `'`)
// io.WriteString(c.w, col.Name)
// io.WriteString(c.w, ` not defined'`)
// alias(c.w, col.Name)
// }
@ -189,10 +190,10 @@ func (c *compilerContext) renderColumnFunction(sel *qcode.Select, ti *DBTableInf
c.renderComma(columnsRendered)
//fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Name, cn, col.Name)
_, _ = io.WriteString(c.w, fn)
_, _ = io.WriteString(c.w, `(`)
io.WriteString(c.w, fn)
io.WriteString(c.w, `(`)
colWithTable(c.w, ti.Name, cn)
_, _ = io.WriteString(c.w, `)`)
io.WriteString(c.w, `)`)
alias(c.w, col.Name)
return nil
@ -200,7 +201,7 @@ func (c *compilerContext) renderColumnFunction(sel *qcode.Select, ti *DBTableInf
func (c *compilerContext) renderComma(columnsRendered int) {
if columnsRendered != 0 {
_, _ = io.WriteString(c.w, `, `)
io.WriteString(c.w, `, `)
}
}

View File

@ -25,7 +25,7 @@ func (c *compilerContext) renderInsert(
if insert[0] == '[' {
io.WriteString(c.w, `json_array_elements(`)
}
c.md.renderValueExp(c.w, Param{Name: qc.ActionVar, Type: "json"})
c.renderValueExp(Param{Name: qc.ActionVar, Type: "json"})
io.WriteString(c.w, ` :: json`)
if insert[0] == '[' {
io.WriteString(c.w, `)`)

View File

@ -1,61 +0,0 @@
package psql
import (
"io"
)
func (md *Metadata) RenderVar(w io.Writer, vv string) {
f, s := -1, 0
for i := range vv {
v := vv[i]
switch {
case (i > 0 && vv[i-1] != '\\' && v == '$') || v == '$':
if (i - s) > 0 {
_, _ = io.WriteString(w, vv[s:i])
}
f = i
case (v < 'a' && v > 'z') &&
(v < 'A' && v > 'Z') &&
(v < '0' && v > '9') &&
v != '_' &&
f != -1 &&
(i-f) > 1:
md.renderValueExp(w, Param{Name: vv[f+1 : i]})
s = i
f = -1
}
}
if f != -1 && (len(vv)-f) > 1 {
md.renderValueExp(w, Param{Name: vv[f+1:]})
} else {
_, _ = io.WriteString(w, vv[s:])
}
}
func (md *Metadata) renderValueExp(w io.Writer, p Param) {
_, _ = io.WriteString(w, `$`)
if v, ok := md.pindex[p.Name]; ok {
int32String(w, int32(v))
} else {
md.params = append(md.params, p)
n := len(md.params)
if md.pindex == nil {
md.pindex = make(map[string]int)
}
md.pindex[p.Name] = n
int32String(w, int32(n))
}
}
func (md Metadata) Skipped() uint32 {
return md.skipped
}
func (md Metadata) Params() []Param {
return md.params
}

View File

@ -432,11 +432,11 @@ func (c *compilerContext) renderInsertUpdateColumns(
val := root.PresetMap[cn]
switch {
case ok && len(val) > 1 && val[0] == '$':
c.md.renderValueExp(c.w, Param{Name: val[1:], Type: col.Type})
c.renderValueExp(Param{Name: val[1:], Type: col.Type})
case ok && strings.HasPrefix(val, "sql:"):
io.WriteString(c.w, `(`)
c.md.RenderVar(c.w, val[4:])
c.renderVar(val[4:], c.renderValueExp)
io.WriteString(c.w, `)`)
case ok:

View File

@ -25,8 +25,8 @@ type Param struct {
}
type Metadata struct {
skipped uint32
params []Param
Skipped uint32
Params []Param
pindex map[string]int
}
@ -80,30 +80,26 @@ func (co *Compiler) CompileEx(qc *qcode.QCode, vars Variables) (Metadata, []byte
}
func (co *Compiler) Compile(w io.Writer, qc *qcode.QCode, vars Variables) (Metadata, error) {
return co.CompileWithMetadata(w, qc, vars, Metadata{})
}
func (co *Compiler) CompileWithMetadata(w io.Writer, qc *qcode.QCode, vars Variables, md Metadata) (Metadata, error) {
md.skipped = 0
if qc == nil {
return md, fmt.Errorf("qcode is nil")
return Metadata{}, fmt.Errorf("qcode is nil")
}
switch qc.Type {
case qcode.QTQuery:
return co.compileQueryWithMetadata(w, qc, vars, md)
return co.compileQuery(w, qc, vars)
case qcode.QTInsert,
qcode.QTUpdate,
qcode.QTDelete,
qcode.QTUpsert:
return co.compileMutation(w, qc, vars)
default:
return Metadata{}, fmt.Errorf("Unknown operation type %d", qc.Type)
}
return Metadata{}, fmt.Errorf("Unknown operation type %d", qc.Type)
}
func (co *Compiler) compileQuery(w io.Writer, qc *qcode.QCode, vars Variables) (Metadata, error) {
return co.compileQueryWithMetadata(w, qc, vars, Metadata{})
}
func (co *Compiler) compileQueryWithMetadata(
@ -156,40 +152,38 @@ func (co *Compiler) compileQueryWithMetadata(
if id < closeBlock {
sel := &c.s[id]
if len(sel.Cols) == 0 {
continue
}
ti, err := c.schema.GetTable(sel.Name)
if err != nil {
return c.md, err
}
if sel.Type != qcode.STUnion {
if len(sel.Cols) == 0 {
continue
}
if sel.ParentID == -1 {
io.WriteString(c.w, `(`)
} else {
c.renderLateralJoin(sel)
}
if sel.ParentID == -1 {
io.WriteString(c.w, `(`)
} else {
c.renderLateralJoin(sel)
}
if !ti.IsSingular {
c.renderPluralSelect(sel, ti)
}
if !ti.IsSingular {
c.renderPluralSelect(sel, ti)
}
if err := c.renderSelect(sel, ti, vars); err != nil {
return c.md, err
}
if err := c.renderSelect(sel, ti, vars); err != nil {
return c.md, err
}
for _, cid := range sel.Children {
if hasBit(c.md.skipped, uint32(cid)) {
if hasBit(c.md.Skipped, uint32(cid)) {
continue
}
child := &c.s[cid]
if child.SkipRender {
continue
}
st.Push(child.ID + closeBlock)
st.Push(child.ID)
}
@ -197,37 +191,35 @@ func (co *Compiler) compileQueryWithMetadata(
} else {
sel := &c.s[(id - closeBlock)]
if sel.Type != qcode.STUnion {
ti, err := c.schema.GetTable(sel.Name)
if err != nil {
return c.md, err
}
io.WriteString(c.w, `)`)
aliasWithID(c.w, "__sr", sel.ID)
io.WriteString(c.w, `)`)
aliasWithID(c.w, "__sj", sel.ID)
if !ti.IsSingular {
io.WriteString(c.w, `)`)
aliasWithID(c.w, "__sj", sel.ID)
}
if sel.ParentID == -1 {
if st.Len() != 0 {
io.WriteString(c.w, `, `)
}
} else {
c.renderLateralJoinClose(sel)
}
ti, err := c.schema.GetTable(sel.Name)
if err != nil {
return c.md, err
}
if sel.Type != qcode.STMember {
if len(sel.Args) != 0 {
for _, v := range sel.Args {
qcode.FreeNode(v)
}
io.WriteString(c.w, `)`)
aliasWithID(c.w, "__sr", sel.ID)
io.WriteString(c.w, `)`)
aliasWithID(c.w, "__sj", sel.ID)
if !ti.IsSingular {
io.WriteString(c.w, `)`)
aliasWithID(c.w, "__sj", sel.ID)
}
if sel.ParentID == -1 {
if st.Len() != 0 {
io.WriteString(c.w, `, `)
}
} else {
c.renderLateralJoinClose(sel)
}
if len(sel.Args) != 0 {
i := 0
for _, v := range sel.Args {
qcode.FreeNode(v, 500)
i++
}
}
}
@ -362,17 +354,7 @@ func (c *compilerContext) initSelect(sel *qcode.Select, ti *DBTableInfo, vars Va
if _, ok := colmap[rel.Left.Col]; !ok {
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Right.Col})
colmap[rel.Left.Col] = struct{}{}
c.md.skipped |= (1 << uint(id))
}
case RelPolymorphic:
if _, ok := colmap[rel.Left.Col]; !ok {
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Left.Col})
colmap[rel.Left.Col] = struct{}{}
}
if _, ok := colmap[rel.Right.Table]; !ok {
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Right.Table, FieldName: rel.Right.Table})
colmap[rel.Right.Table] = struct{}{}
c.md.Skipped |= (1 << uint(id))
}
default:
@ -453,23 +435,15 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars
var rel *DBRel
var err error
// Relationships must be between union parents and their parents
if sel.ParentID != -1 {
if sel.Type == qcode.STMember && sel.UParentID != -1 {
cn := c.s[sel.ParentID].Name
pn := c.s[sel.UParentID].Name
rel, err = c.schema.GetRel(cn, pn)
parent := c.s[sel.ParentID]
} else {
pn := c.s[sel.ParentID].Name
rel, err = c.schema.GetRel(ti.Name, pn)
rel, err = c.schema.GetRel(ti.Name, parent.Name)
if err != nil {
return err
}
}
if err != nil {
return err
}
childCols, err := c.initSelect(sel, ti, vars)
if err != nil {
return err
@ -553,27 +527,30 @@ func (c *compilerContext) renderJoin(sel *qcode.Select, ti *DBTableInfo) error {
}
func (c *compilerContext) renderJoinByName(table, parent string, id int32) error {
rel, _ := c.schema.GetRel(table, parent)
rel, err := c.schema.GetRel(table, parent)
if err != nil {
return err
}
// This join is only required for one-to-many relations since
// these make use of join tables that need to be pulled in.
if rel == nil || rel.Type != RelOneToManyThrough {
return nil
if rel.Type != RelOneToManyThrough {
return err
}
// pt, err := c.schema.GetTable(parent)
// if err != nil {
// return err
// }
pt, err := c.schema.GetTable(parent)
if err != nil {
return err
}
//fmt.Fprintf(w, ` LEFT OUTER JOIN "%s" ON (("%s"."%s") = ("%s_%d"."%s"))`,
//rel.Through, rel.Through, rel.ColT, c.parent.Name, c.parent.ID, rel.Left.Col)
io.WriteString(c.w, ` LEFT OUTER JOIN "`)
io.WriteString(c.w, rel.Through.Table)
io.WriteString(c.w, rel.Through)
io.WriteString(c.w, `" ON ((`)
colWithTable(c.w, rel.Through.Table, rel.Through.ColL)
colWithTable(c.w, rel.Through, rel.ColT)
io.WriteString(c.w, `) = (`)
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
colWithTableID(c.w, pt.Name, id, rel.Left.Col)
io.WriteString(c.w, `))`)
return nil
@ -645,7 +622,7 @@ func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo,
i := colsRendered
for _, id := range sel.Children {
if hasBit(c.md.skipped, uint32(id)) {
if hasBit(c.md.Skipped, uint32(id)) {
continue
}
childSel := &c.s[id]
@ -660,33 +637,10 @@ func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo,
continue
}
if childSel.Type == qcode.STUnion {
rel, err := c.schema.GetRel(childSel.Name, ti.Name)
if err != nil {
return err
}
io.WriteString(c.w, `(CASE `)
for _, uid := range childSel.Children {
unionSel := &c.s[uid]
io.WriteString(c.w, `WHEN `)
colWithTableID(c.w, ti.Name, sel.ID, rel.Right.Table)
io.WriteString(c.w, ` = `)
squoted(c.w, unionSel.Name)
io.WriteString(c.w, ` THEN `)
io.WriteString(c.w, `"__sj_`)
int32String(c.w, unionSel.ID)
io.WriteString(c.w, `"."json"`)
}
io.WriteString(c.w, `END)`)
alias(c.w, childSel.FieldName)
} else {
io.WriteString(c.w, `"__sj_`)
int32String(c.w, childSel.ID)
io.WriteString(c.w, `"."json"`)
alias(c.w, childSel.FieldName)
}
io.WriteString(c.w, `"__sj_`)
int32String(c.w, childSel.ID)
io.WriteString(c.w, `"."json"`)
alias(c.w, childSel.FieldName)
if childSel.Paging.Type != qcode.PtOffset {
io.WriteString(c.w, `, "__sj_`)
@ -741,8 +695,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
}
io.WriteString(c.w, ` WHERE (`)
if err := c.renderRelationship(sel, rel); err != nil {
if err := c.renderRelationship(sel, ti); err != nil {
return err
}
if isFil {
@ -774,7 +727,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
case ti.IsSingular:
io.WriteString(c.w, ` LIMIT ('1') :: integer`)
case sel.Paging.Limit != "":
case len(sel.Paging.Limit) != 0:
//fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit)
io.WriteString(c.w, ` LIMIT ('`)
io.WriteString(c.w, sel.Paging.Limit)
@ -787,7 +740,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
io.WriteString(c.w, ` LIMIT ('20') :: integer`)
}
if sel.Paging.Offset != "" {
if len(sel.Paging.Offset) != 0 {
//fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, c.sel.Paging.Offset)
io.WriteString(c.w, ` OFFSET ('`)
io.WriteString(c.w, sel.Paging.Offset)
@ -851,29 +804,26 @@ func (c *compilerContext) renderCursorCTE(sel *qcode.Select) error {
quoted(c.w, ob.Col)
}
io.WriteString(c.w, ` FROM string_to_array(`)
c.md.renderValueExp(c.w, Param{Name: "cursor", Type: "json"})
c.renderValueExp(Param{Name: "cursor", Type: "json"})
io.WriteString(c.w, `, ',') as a) `)
return nil
}
func (c *compilerContext) renderRelationshipByName(table, parent string) error {
rel, err := c.schema.GetRel(table, parent)
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
parent := c.s[sel.ParentID]
pti, err := c.schema.GetTable(parent.Name)
if err != nil {
return err
}
return c.renderRelationship(nil, rel)
return c.renderRelationshipByName(ti.Name, pti.Name, parent.ID)
}
func (c *compilerContext) renderRelationship(sel *qcode.Select, rel *DBRel) error {
var pid int32
switch {
case sel == nil:
pid = int32(-1)
case sel.Type == qcode.STMember:
pid = sel.UParentID
default:
pid = sel.ParentID
func (c *compilerContext) renderRelationshipByName(table, parent string, id int32) error {
rel, err := c.schema.GetRel(table, parent)
if err != nil {
return err
}
io.WriteString(c.w, `((`)
@ -886,19 +836,19 @@ func (c *compilerContext) renderRelationship(sel *qcode.Select, rel *DBRel) erro
switch {
case !rel.Left.Array && rel.Right.Array:
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
colWithTable(c.w, table, rel.Left.Col)
io.WriteString(c.w, `) = any (`)
colWithTableID(c.w, rel.Right.Table, pid, rel.Right.Col)
colWithTableID(c.w, parent, id, rel.Right.Col)
case rel.Left.Array && !rel.Right.Array:
colWithTableID(c.w, rel.Right.Table, pid, rel.Right.Col)
colWithTableID(c.w, parent, id, rel.Right.Col)
io.WriteString(c.w, `) = any (`)
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
colWithTable(c.w, table, rel.Left.Col)
default:
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
colWithTable(c.w, table, rel.Left.Col)
io.WriteString(c.w, `) = (`)
colWithTableID(c.w, rel.Right.Table, pid, rel.Right.Col)
colWithTableID(c.w, parent, id, rel.Right.Col)
}
case RelOneToManyThrough:
@ -908,34 +858,25 @@ func (c *compilerContext) renderRelationship(sel *qcode.Select, rel *DBRel) erro
switch {
case !rel.Left.Array && rel.Right.Array:
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
colWithTable(c.w, table, rel.Left.Col)
io.WriteString(c.w, `) = any (`)
colWithTable(c.w, rel.Through.Table, rel.Through.ColR)
colWithTable(c.w, rel.Through, rel.Right.Col)
case rel.Left.Array && !rel.Right.Array:
colWithTable(c.w, rel.Through.Table, rel.Through.ColR)
colWithTable(c.w, rel.Through, rel.Right.Col)
io.WriteString(c.w, `) = any (`)
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
colWithTable(c.w, table, rel.Left.Col)
default:
colWithTable(c.w, rel.Through.Table, rel.Through.ColR)
colWithTable(c.w, table, rel.Left.Col)
io.WriteString(c.w, `) = (`)
colWithTable(c.w, rel.Right.Table, rel.Right.Col)
colWithTable(c.w, rel.Through, rel.Right.Col)
}
case RelEmbedded:
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
io.WriteString(c.w, `) = (`)
colWithTableID(c.w, rel.Left.Table, pid, rel.Left.Col)
case RelPolymorphic:
colWithTable(c.w, sel.Name, rel.Right.Col)
io.WriteString(c.w, `) = (`)
colWithTableID(c.w, rel.Left.Table, pid, rel.Left.Col)
io.WriteString(c.w, `) AND (`)
colWithTableID(c.w, rel.Left.Table, pid, rel.Right.Table)
io.WriteString(c.w, `) = (`)
squoted(c.w, sel.Name)
colWithTableID(c.w, parent, id, rel.Left.Col)
}
io.WriteString(c.w, `))`)
@ -1011,8 +952,11 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b
return err
}
} else if err := c.renderOp(val, ti); err != nil {
return err
} else {
//fmt.Fprintf(w, `(("%s"."%s") `, c.sel.Name, val.Col)
if err := c.renderOp(val, ti); err != nil {
return err
}
}
}
//qcode.FreeExp(val)
@ -1045,7 +989,7 @@ func (c *compilerContext) renderNestedWhere(ex *qcode.Exp, ti *DBTableInfo) erro
io.WriteString(c.w, ` WHERE `)
if err := c.renderRelationshipByName(cti.Name, ti.Name); err != nil {
if err := c.renderRelationshipByName(cti.Name, ti.Name, -1); err != nil {
return err
}
@ -1074,7 +1018,7 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error {
return nil
}
if ex.Col != "" {
if len(ex.Col) != 0 {
if col, ok = ti.ColMap[ex.Col]; !ok {
return fmt.Errorf("no column '%s' found ", ex.Col)
}
@ -1158,7 +1102,7 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error {
} else {
io.WriteString(c.w, `) @@ to_tsquery(`)
}
c.md.renderValueExp(c.w, Param{Name: ex.Val, Type: "string"})
c.renderValueExp(Param{Name: ex.Val, Type: "string"})
io.WriteString(c.w, `))`)
return nil
@ -1247,7 +1191,7 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *
switch {
case ok && strings.HasPrefix(val, "sql:"):
io.WriteString(c.w, `(`)
c.md.RenderVar(c.w, val[4:])
c.renderVar(val[4:], c.renderValueExp)
io.WriteString(c.w, `)`)
case ok:
@ -1255,7 +1199,7 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *
case ex.Op == qcode.OpIn || ex.Op == qcode.OpNotIn:
io.WriteString(c.w, `(ARRAY(SELECT json_array_elements_text(`)
c.md.renderValueExp(c.w, Param{Name: ex.Val, Type: col.Type, IsArray: true})
c.renderValueExp(Param{Name: ex.Val, Type: col.Type, IsArray: true})
io.WriteString(c.w, `))`)
io.WriteString(c.w, ` :: `)
@ -1264,7 +1208,7 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *
return
default:
c.md.renderValueExp(c.w, Param{Name: ex.Val, Type: col.Type, IsArray: false})
c.renderValueExp(Param{Name: ex.Val, Type: col.Type, IsArray: false})
}
case qcode.ValRef:
@ -1278,6 +1222,54 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *
io.WriteString(c.w, col.Type)
}
func (c *compilerContext) renderValueExp(p Param) {
io.WriteString(c.w, `$`)
if v, ok := c.md.pindex[p.Name]; ok {
int32String(c.w, int32(v))
} else {
c.md.Params = append(c.md.Params, p)
n := len(c.md.Params)
if c.md.pindex == nil {
c.md.pindex = make(map[string]int)
}
c.md.pindex[p.Name] = n
int32String(c.w, int32(n))
}
}
func (c *compilerContext) renderVar(vv string, fn func(Param)) {
f, s := -1, 0
for i := range vv {
v := vv[i]
switch {
case (i > 0 && vv[i-1] != '\\' && v == '$') || v == '$':
if (i - s) > 0 {
io.WriteString(c.w, vv[s:i])
}
f = i
case (v < 'a' && v > 'z') &&
(v < 'A' && v > 'Z') &&
(v < '0' && v > '9') &&
v != '_' &&
f != -1 &&
(i-f) > 1:
fn(Param{Name: vv[f+1 : i]})
s = i
f = -1
}
}
if f != -1 && (len(vv)-f) > 1 {
fn(Param{Name: vv[f+1:]})
} else {
io.WriteString(c.w, vv[s:])
}
}
func funcPrefixLen(fm map[string]*DBFunction, fn string) int {
switch {
case strings.HasPrefix(fn, "avg_"):
@ -1314,7 +1306,7 @@ func funcPrefixLen(fm map[string]*DBFunction, fn string) int {
return 0
}
func hasBit(n, pos uint32) bool {
func hasBit(n uint32, pos uint32) bool {
val := n & (1 << pos)
return (val > 0)
}

View File

@ -307,100 +307,6 @@ func multiRoot(t *testing.T) {
compileGQLToPSQL(t, gql, nil, "user")
}
func withFragment1(t *testing.T) {
gql := `
fragment userFields1 on user {
id
email
}
query {
users {
...userFields2
created_at
...userFields1
}
}
fragment userFields2 on user {
first_name
last_name
}`
compileGQLToPSQL(t, gql, nil, "anon")
}
func withFragment2(t *testing.T) {
gql := `
query {
users {
...userFields2
created_at
...userFields1
}
}
fragment userFields1 on user {
id
email
}
fragment userFields2 on user {
first_name
last_name
}`
compileGQLToPSQL(t, gql, nil, "anon")
}
func withFragment3(t *testing.T) {
gql := `
fragment userFields1 on user {
id
email
}
fragment userFields2 on user {
first_name
last_name
}
query {
users {
...userFields2
created_at
...userFields1
}
}
`
compileGQLToPSQL(t, gql, nil, "anon")
}
// func withInlineFragment(t *testing.T) {
// gql := `
// query {
// users {
// ... on users {
// id
// email
// }
// created_at
// ... on user {
// first_name
// last_name
// }
// }
// }
// `
// compileGQLToPSQL(t, gql, nil, "anon")
// }
func withCursor(t *testing.T) {
gql := `query {
Products(
@ -494,10 +400,6 @@ func TestCompileQuery(t *testing.T) {
t.Run("queryWithVariables", queryWithVariables)
t.Run("withWhereOnRelations", withWhereOnRelations)
t.Run("multiRoot", multiRoot)
t.Run("withFragment1", withFragment1)
t.Run("withFragment2", withFragment2)
t.Run("withFragment3", withFragment3)
//t.Run("withInlineFragment", withInlineFragment)
t.Run("jsonColumnAsTable", jsonColumnAsTable)
t.Run("withCursor", withCursor)
t.Run("nullForAuthRequiredInAnon", nullForAuthRequiredInAnon)

View File

@ -11,7 +11,6 @@ type DBSchema struct {
ver int
t map[string]*DBTableInfo
rm map[string]map[string]*DBRel
vt map[string]*VirtualTable
fm map[string]*DBFunction
}
@ -34,19 +33,15 @@ const (
RelOneToOne RelType = iota + 1
RelOneToMany
RelOneToManyThrough
RelPolymorphic
RelEmbedded
RelRemote
)
type DBRel struct {
Type RelType
Through struct {
Table string
ColL string
ColR string
}
Left struct {
Through string
ColT string
Left struct {
col *DBColumn
Table string
Col string
@ -65,7 +60,6 @@ func NewDBSchema(info *DBInfo, aliases map[string][]string) (*DBSchema, error) {
ver: info.Version,
t: make(map[string]*DBTableInfo),
rm: make(map[string]map[string]*DBRel),
vt: make(map[string]*VirtualTable),
fm: make(map[string]*DBFunction, len(info.Functions)),
}
@ -76,10 +70,6 @@ func NewDBSchema(info *DBInfo, aliases map[string][]string) (*DBSchema, error) {
}
}
if err := schema.virtualRels(info.VTables); err != nil {
return nil, err
}
for i, t := range info.Tables {
err := schema.firstDegreeRels(t, info.Columns[i])
if err != nil {
@ -112,7 +102,7 @@ func (s *DBSchema) addTable(
singular := flect.Singularize(t.Key)
plural := flect.Pluralize(t.Key)
ts := &DBTableInfo{
s.t[singular] = &DBTableInfo{
Name: t.Name,
Type: t.Type,
IsSingular: true,
@ -122,9 +112,8 @@ func (s *DBSchema) addTable(
Singular: singular,
Plural: plural,
}
s.t[singular] = ts
tp := &DBTableInfo{
s.t[plural] = &DBTableInfo{
Name: t.Name,
Type: t.Type,
IsSingular: false,
@ -134,15 +123,14 @@ func (s *DBSchema) addTable(
Singular: singular,
Plural: plural,
}
s.t[plural] = tp
if al, ok := aliases[t.Key]; ok {
for i := range al {
k1 := flect.Singularize(al[i])
s.t[k1] = ts
s.t[k1] = s.t[singular]
k2 := flect.Pluralize(al[i])
s.t[k2] = tp
s.t[k2] = s.t[plural]
}
}
@ -166,54 +154,6 @@ func (s *DBSchema) addTable(
return nil
}
func (s *DBSchema) virtualRels(vts []VirtualTable) error {
for _, vt := range vts {
s.vt[vt.Name] = &vt
for _, t := range s.t {
idCol, ok := t.ColMap[vt.IDColumn]
if !ok {
continue
}
if _, ok = t.ColMap[vt.TypeColumn]; !ok {
continue
}
nt := DBTable{
ID: -1,
Name: vt.Name,
Key: strings.ToLower(vt.Name),
Type: "virtual",
}
if err := s.addTable(nt, nil, nil); err != nil {
return err
}
rel := &DBRel{Type: RelPolymorphic}
rel.Left.col = idCol
rel.Left.Table = t.Name
rel.Left.Col = idCol.Name
rcol := DBColumn{
Name: vt.FKeyColumn,
Key: strings.ToLower(vt.FKeyColumn),
Type: idCol.Type,
}
rel.Right.col = &rcol
rel.Right.Table = vt.TypeColumn
rel.Right.Col = rcol.Name
if err := s.SetRel(vt.Name, t.Name, rel); err != nil {
return err
}
}
}
return nil
}
func (s *DBSchema) firstDegreeRels(t DBTable, cols []DBColumn) error {
ct := t.Key
cti, ok := s.t[ct]
@ -224,7 +164,7 @@ func (s *DBSchema) firstDegreeRels(t DBTable, cols []DBColumn) error {
for i := range cols {
c := cols[i]
if c.FKeyTable == "" {
if len(c.FKeyTable) == 0 {
continue
}
@ -328,7 +268,7 @@ func (s *DBSchema) secondDegreeRels(t DBTable, cols []DBColumn) error {
for i := range cols {
c := cols[i]
if c.FKeyTable == "" {
if len(c.FKeyTable) == 0 {
continue
}
@ -404,17 +344,16 @@ func (s *DBSchema) updateSchemaOTMT(
// One-to-many-through relation between 1nd foreign key table and the
// 2nd foreign key table
rel1 := &DBRel{Type: RelOneToManyThrough}
rel1.Through.Table = ti.Name
rel1.Through.ColL = col1.Name
rel1.Through.ColR = col2.Name
rel1.Through = ti.Name
rel1.ColT = col2.Name
rel1.Left.col = fc1
rel1.Left.Table = col1.FKeyTable
rel1.Left.Col = fc1.Name
rel1.Left.col = &col2
rel1.Left.Table = col2.FKeyTable
rel1.Left.Col = fc2.Name
rel1.Right.col = fc2
rel1.Right.Table = t2
rel1.Right.Col = fc2.Name
rel1.Right.col = &col1
rel1.Right.Table = ti.Name
rel1.Right.Col = col1.Name
if err := s.SetRel(t1, t2, rel1); err != nil {
return err
@ -423,17 +362,16 @@ func (s *DBSchema) updateSchemaOTMT(
// One-to-many-through relation between 2nd foreign key table and the
// 1nd foreign key table
rel2 := &DBRel{Type: RelOneToManyThrough}
rel2.Through.Table = ti.Name
rel2.Through.ColL = col2.Name
rel2.Through.ColR = col1.Name
rel2.Through = ti.Name
rel2.ColT = col1.Name
rel2.Left.col = fc2
rel2.Left.Table = col2.FKeyTable
rel2.Left.Col = fc2.Name
rel1.Left.col = fc1
rel2.Left.Table = col1.FKeyTable
rel2.Left.Col = fc1.Name
rel2.Right.col = fc1
rel2.Right.Table = t1
rel2.Right.Col = fc1.Name
rel1.Right.col = &col2
rel2.Right.Table = ti.Name
rel2.Right.Col = col2.Name
if err := s.SetRel(t2, t1, rel2); err != nil {
return err

View File

@ -14,18 +14,14 @@ func (rt RelType) String() string {
return "remote"
case RelEmbedded:
return "embedded"
case RelPolymorphic:
return "polymorphic"
}
return ""
}
func (re *DBRel) String() string {
if re.Type == RelOneToManyThrough {
return fmt.Sprintf("'%s.%s' --(%s.%s, %s.%s)--> '%s.%s'",
re.Left.Table, re.Left.Col,
re.Through.Table, re.Through.ColL, re.Through.Table, re.Through.ColR,
re.Right.Table, re.Right.Col)
return fmt.Sprintf("'%s.%s' --(Through: %s)--> '%s.%s'",
re.Left.Table, re.Left.Col, re.Through, re.Right.Table, re.Right.Col)
}
return fmt.Sprintf("'%s.%s' --(%s)--> '%s.%s'",
re.Left.Table, re.Left.Col, re.Type, re.Right.Table, re.Right.Col)

View File

@ -14,17 +14,9 @@ type DBInfo struct {
Tables []DBTable
Columns [][]DBColumn
Functions []DBFunction
VTables []VirtualTable
colMap map[string]map[string]*DBColumn
}
type VirtualTable struct {
Name string
IDColumn string
TypeColumn string
FKeyColumn string
}
func GetDBInfo(db *sql.DB, schema string) (*DBInfo, error) {
di := &DBInfo{}
var version string

View File

@ -8,8 +8,8 @@ WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "produc
=== RUN TestCompileInsert/simpleInsertWithPresets
WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, $2 :: bigint FROM "_sg_input" i RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
=== RUN TestCompileInsert/nestedInsertManyToMany
WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "products" ("name", "price") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT CAST( i.j ->>'sale_type' AS character varying), CAST( i.j ->>'quantity' AS integer), CAST( i.j ->>'due_date' AS timestamp without time zone), "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products" RETURNING *) SELECT jsonb_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
WITH "_sg_input" AS (SELECT $1 :: json AS j), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i RETURNING *), "products" AS (INSERT INTO "products" ("name", "price") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "product_id", "customer_id") SELECT CAST( i.j ->>'sale_type' AS character varying), CAST( i.j ->>'quantity' AS integer), CAST( i.j ->>'due_date' AS timestamp without time zone), "products"."id", "customers"."id" FROM "_sg_input" i, "products", "customers" RETURNING *) SELECT jsonb_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "products" ("name", "price") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT CAST( i.j ->>'sale_type' AS character varying), CAST( i.j ->>'quantity' AS integer), CAST( i.j ->>'due_date' AS timestamp without time zone), "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products" RETURNING *) SELECT jsonb_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
=== RUN TestCompileInsert/nestedInsertOneToMany
WITH "_sg_input" AS (SELECT $1 :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone), "users"."id" FROM "_sg_input" i, "users" RETURNING *) SELECT jsonb_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
=== RUN TestCompileInsert/nestedInsertOneToOne
@ -20,7 +20,7 @@ WITH "_sg_input" AS (SELECT $1 :: json AS j), "users" AS (INSERT INTO "users" ("
WITH "_sg_input" AS (SELECT $1 :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone), "_x_users"."id" FROM "_sg_input" i, "_x_users" RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user", "__sj_2"."json" AS "tags" FROM (SELECT "products"."id", "products"."name", "products"."user_id", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "tags_2"."id" AS "id", "tags_2"."name" AS "name" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
=== RUN TestCompileInsert/nestedInsertOneToOneWithConnectArray
WITH "_sg_input" AS (SELECT $1 :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id" = ANY((select a::bigint AS list from json_array_elements_text((i.j->'user'->'connect'->>'id')::json) AS a)) LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone), "_x_users"."id" FROM "_sg_input" i, "_x_users" RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
--- PASS: TestCompileInsert (0.03s)
--- PASS: TestCompileInsert (0.02s)
--- PASS: TestCompileInsert/simpleInsert (0.00s)
--- PASS: TestCompileInsert/singleInsert (0.00s)
--- PASS: TestCompileInsert/bulkInsert (0.00s)
@ -67,9 +67,9 @@ SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT
=== RUN TestCompileQuery/oneToManyArray
SELECT jsonb_build_object('tags', "__sj_0"."json", 'product', "__sj_2"."json") as "__root" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "products_2"."name" AS "name", "products_2"."price" AS "price", "__sj_3"."json" AS "tags" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_3"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "tags_3"."id" AS "id", "tags_3"."name" AS "name" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3") AS "__sr_3") AS "__sj_3") AS "__sj_3" ON ('true')) AS "__sr_2") AS "__sj_2", (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "tags_0"."name" AS "name", "__sj_1"."json" AS "product" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."name" AS "name" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
=== RUN TestCompileQuery/manyToMany
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."name" AS "name", "__sj_1"."json" AS "customers" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers"."id")) WHERE ((("purchases"."product_id") = ("products"."id"))) LIMIT ('20') :: integer) AS "customers_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."name" AS "name", "__sj_1"."json" AS "customers" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
=== RUN TestCompileQuery/manyToManyReverse
SELECT jsonb_build_object('customers', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "__sj_1"."json" AS "products" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."name" AS "name" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products"."id")) WHERE ((("purchases"."customer_id") = ("customers"."id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
SELECT jsonb_build_object('customers', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "__sj_1"."json" AS "products" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."name" AS "name" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
=== RUN TestCompileQuery/aggFunction
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
=== RUN TestCompileQuery/aggFunctionBlockedByCol
@ -85,13 +85,7 @@ SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT t
=== RUN TestCompileQuery/withWhereOnRelations
SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > '3' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
=== RUN TestCompileQuery/multiRoot
SELECT jsonb_build_object('customer', "__sj_0"."json", 'user', "__sj_1"."json", 'product', "__sj_2"."json") as "__root" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "products_2"."id" AS "id", "products_2"."name" AS "name", "__sj_3"."json" AS "customers", "__sj_4"."json" AS "customer" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_4".*) AS "json"FROM (SELECT "customers_4"."email" AS "email" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers"."id")) WHERE ((("purchases"."product_id") = ("products"."id"))) LIMIT ('1') :: integer) AS "customers_4") AS "__sr_4") AS "__sj_4" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_3"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "customers_3"."email" AS "email" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers"."id")) WHERE ((("purchases"."product_id") = ("products"."id"))) LIMIT ('20') :: integer) AS "customers_3") AS "__sr_3") AS "__sj_3") AS "__sj_3" ON ('true')) AS "__sr_2") AS "__sj_2", (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1", (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "customers_0"."id" AS "id" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0") AS "__sr_0") AS "__sj_0"
=== RUN TestCompileQuery/withFragment1
SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."first_name" AS "first_name", "users_0"."last_name" AS "last_name", "users_0"."created_at" AS "created_at", "users_0"."id" AS "id", "users_0"."email" AS "email" FROM (SELECT , "users"."created_at", "users"."id", "users"."email" FROM "users" GROUP BY "users"."created_at", "users"."id", "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
=== RUN TestCompileQuery/withFragment2
SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."first_name" AS "first_name", "users_0"."last_name" AS "last_name", "users_0"."created_at" AS "created_at", "users_0"."id" AS "id", "users_0"."email" AS "email" FROM (SELECT , "users"."created_at", "users"."id", "users"."email" FROM "users" GROUP BY "users"."created_at", "users"."id", "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
=== RUN TestCompileQuery/withFragment3
SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."first_name" AS "first_name", "users_0"."last_name" AS "last_name", "users_0"."created_at" AS "created_at", "users_0"."id" AS "id", "users_0"."email" AS "email" FROM (SELECT , "users"."created_at", "users"."id", "users"."email" FROM "users" GROUP BY "users"."created_at", "users"."id", "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
SELECT jsonb_build_object('customer', "__sj_0"."json", 'user', "__sj_1"."json", 'product', "__sj_2"."json") as "__root" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "products_2"."id" AS "id", "products_2"."name" AS "name", "__sj_3"."json" AS "customers", "__sj_4"."json" AS "customer" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_4".*) AS "json"FROM (SELECT "customers_4"."email" AS "email" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4") AS "__sr_4") AS "__sj_4" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_3"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "customers_3"."email" AS "email" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3") AS "__sr_3") AS "__sj_3") AS "__sj_3" ON ('true')) AS "__sr_2") AS "__sj_2", (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1", (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "customers_0"."id" AS "id" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0") AS "__sr_0") AS "__sj_0"
=== RUN TestCompileQuery/jsonColumnAsTable
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "tag_count" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "tag_count_1"."count" AS "count", "__sj_2"."json" AS "tags" FROM (SELECT "tag_count"."count", "tag_count"."tag_id" FROM "products", json_to_recordset("products"."tag_count") AS "tag_count"(tag_id bigint, count int) WHERE ((("products"."id") = ("products_0"."id"))) LIMIT ('1') :: integer) AS "tag_count_1" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "tags_2"."name" AS "name" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true')) AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
=== RUN TestCompileQuery/withCursor
@ -123,9 +117,6 @@ SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coa
--- PASS: TestCompileQuery/queryWithVariables (0.00s)
--- PASS: TestCompileQuery/withWhereOnRelations (0.00s)
--- PASS: TestCompileQuery/multiRoot (0.00s)
--- PASS: TestCompileQuery/withFragment1 (0.00s)
--- PASS: TestCompileQuery/withFragment2 (0.00s)
--- PASS: TestCompileQuery/withFragment3 (0.00s)
--- PASS: TestCompileQuery/jsonColumnAsTable (0.00s)
--- PASS: TestCompileQuery/withCursor (0.00s)
--- PASS: TestCompileQuery/nullForAuthRequiredInAnon (0.00s)
@ -146,8 +137,8 @@ WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (UPDATE "products" S
=== RUN TestCompileUpdate/nestedUpdateOneToManyWithConnect
WITH "_sg_input" AS (SELECT $1 :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i) WHERE (("users"."id") = $2 :: bigint) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id"= ((i.j->'product'->'disconnect'->>'id'))::bigint) RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") SELECT jsonb_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
=== RUN TestCompileUpdate/nestedUpdateOneToOneWithConnect
WITH "_sg_input" AS (SELECT $1 :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying AND "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), "_x_users"."id" FROM "_sg_input" i, "_x_users") WHERE (("products"."id") = $2 :: bigint) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
WITH "_sg_input" AS (SELECT $1 :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint AND "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), "_x_users"."id" FROM "_sg_input" i, "_x_users") WHERE (("products"."id") = $2 :: bigint) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
WITH "_sg_input" AS (SELECT $1 :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying AND "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), "_x_users"."id" FROM "_sg_input" i, "_x_users") WHERE (("products"."id") = $2 :: bigint) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
=== RUN TestCompileUpdate/nestedUpdateOneToOneWithDisconnect
WITH "_sg_input" AS (SELECT $1 :: json AS j), "_x_users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), "_x_users"."id" FROM "_sg_input" i, "_x_users") WHERE (("products"."id") = $2 :: bigint) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
--- PASS: TestCompileUpdate (0.02s)
@ -160,4 +151,4 @@ WITH "_sg_input" AS (SELECT $1 :: json AS j), "_x_users" AS (SELECT * FROM (VALU
--- PASS: TestCompileUpdate/nestedUpdateOneToOneWithConnect (0.00s)
--- PASS: TestCompileUpdate/nestedUpdateOneToOneWithDisconnect (0.00s)
PASS
ok github.com/dosco/super-graph/core/internal/psql 0.323s
ok github.com/dosco/super-graph/core/internal/psql (cached)

View File

@ -22,7 +22,7 @@ func (c *compilerContext) renderUpdate(
}
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT `)
c.md.renderValueExp(c.w, Param{Name: qc.ActionVar, Type: "json"})
c.renderValueExp(Param{Name: qc.ActionVar, Type: "json"})
// io.WriteString(c.w, qc.ActionVar)
io.WriteString(c.w, ` :: json AS j)`)
@ -121,10 +121,12 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re
}
io.WriteString(w, `)`)
} else if qc.Selects[0].Where != nil {
io.WriteString(w, `WHERE `)
if err := c.renderWhere(&qc.Selects[0], ti); err != nil {
return err
} else {
if qc.Selects[0].Where != nil {
io.WriteString(w, `WHERE `)
if err := c.renderWhere(&qc.Selects[0], ti); err != nil {
return err
}
}
}

View File

@ -1,13 +0,0 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/core/internal/qcode
BenchmarkQCompile-16 118282 9686 ns/op 4031 B/op 30 allocs/op
BenchmarkQCompileP-16 427531 2710 ns/op 4077 B/op 30 allocs/op
BenchmarkQCompileFragment-16 140588 8328 ns/op 8903 B/op 13 allocs/op
BenchmarkParse-16 131396 9212 ns/op 4175 B/op 18 allocs/op
BenchmarkParseP-16 503778 2310 ns/op 4176 B/op 18 allocs/op
BenchmarkParseFragment-16 143725 8158 ns/op 10193 B/op 9 allocs/op
BenchmarkSchemaParse-16 240609 5060 ns/op 3968 B/op 57 allocs/op
BenchmarkSchemaParseP-16 785116 1534 ns/op 3968 B/op 57 allocs/op
PASS
ok github.com/dosco/super-graph/core/internal/qcode 11.092s

View File

@ -11,18 +11,15 @@ import (
var (
queryToken = []byte("query")
mutationToken = []byte("mutation")
fragmentToken = []byte("fragment")
subscriptionToken = []byte("subscription")
onToken = []byte("on")
trueToken = []byte("true")
falseToken = []byte("false")
quotesToken = []byte(`'"`)
signsToken = []byte(`+-`)
punctuatorToken = []byte(`!():=[]{|}`)
spreadToken = []byte(`...`)
digitToken = []byte(`0123456789`)
dotToken = []byte(`.`)
punctuatorToken = `!():=[]{|}`
)
// Pos represents a byte position in the original input text from which
@ -46,8 +43,6 @@ const (
itemName
itemQuery
itemMutation
itemFragment
itemOn
itemSub
itemPunctuator
itemArgsOpen
@ -141,7 +136,8 @@ func (l *lexer) current() (Pos, Pos) {
func (l *lexer) emit(t itemType) {
l.items = append(l.items, item{t, l.start, l.pos, l.line})
// Some items contain text internally. If so, count their newlines.
if t == itemStringVal {
switch t {
case itemStringVal:
for i := l.start; i < l.pos; i++ {
if l.input[i] == '\n' {
l.line++
@ -267,11 +263,11 @@ func lexRoot(l *lexer) stateFn {
l.backup()
return lexString
case r == '.':
l.acceptRun(dotToken)
s, e := l.current()
if equals(l.input, s, e, spreadToken) {
l.emit(itemSpread)
return lexRoot
if len(l.input) >= 3 {
if equals(l.input, 0, 3, spreadToken) {
l.emit(itemSpread)
return lexRoot
}
}
fallthrough // '.' can start a number.
case r == '+' || r == '-' || ('0' <= r && r <= '9'):
@ -303,14 +299,10 @@ func lexName(l *lexer) stateFn {
switch {
case equals(l.input, s, e, queryToken):
l.emitL(itemQuery)
case equals(l.input, s, e, fragmentToken):
l.emitL(itemFragment)
case equals(l.input, s, e, mutationToken):
l.emitL(itemMutation)
case equals(l.input, s, e, subscriptionToken):
l.emitL(itemSub)
case equals(l.input, s, e, onToken):
l.emitL(itemOn)
case equals(l.input, s, e, trueToken):
l.emitL(itemBoolVal)
case equals(l.input, s, e, falseToken):
@ -403,15 +395,35 @@ func isAlphaNumeric(r rune) bool {
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
}
func equals(b []byte, s, e Pos, val []byte) bool {
return bytes.EqualFold(b[s:e], val)
func equals(b []byte, s Pos, e Pos, val []byte) bool {
n := 0
for i := s; i < e; i++ {
if n >= len(val) {
return true
}
switch {
case b[i] >= 'A' && b[i] <= 'Z' && ('a'+(b[i]-'A')) != val[n]:
return false
case b[i] != val[n]:
return false
}
n++
}
return true
}
func contains(b []byte, s, e Pos, chars string) bool {
return bytes.ContainsAny(b[s:e], chars)
func contains(b []byte, s Pos, e Pos, val []byte) bool {
for i := s; i < e; i++ {
for n := 0; n < len(val); n++ {
if b[i] == val[n] {
return true
}
}
}
return false
}
func lowercase(b []byte, s, e Pos) {
func lowercase(b []byte, s Pos, e Pos) {
for i := s; i < e; i++ {
if b[i] >= 'A' && b[i] <= 'Z' {
b[i] = ('a' + (b[i] - 'A'))

View File

@ -3,9 +3,10 @@ package qcode
import (
"errors"
"fmt"
"hash/maphash"
"sync"
"unsafe"
"github.com/dosco/super-graph/core/internal/util"
)
var (
@ -49,19 +50,6 @@ func (o *Operation) Reset() {
*o = zeroOperation
}
type Fragment struct {
Name string
On string
Fields []Field
fieldsA [10]Field
}
var zeroFragment = Fragment{}
func (f *Fragment) Reset() {
*f = zeroFragment
}
type Field struct {
ID int32
ParentID int32
@ -71,13 +59,11 @@ type Field struct {
argsA [5]Arg
Children []int32
childrenA [5]int32
Union bool
}
type Arg struct {
Name string
Val *Node
df bool
}
type Node struct {
@ -96,8 +82,6 @@ func (n *Node) Reset() {
}
type Parser struct {
frags map[uint64]*Fragment
h maphash.Hash
input []byte // the string being scanned
pos int
items []item
@ -112,192 +96,12 @@ var opPool = sync.Pool{
New: func() interface{} { return new(Operation) },
}
var fragPool = sync.Pool{
New: func() interface{} { return new(Fragment) },
}
var lexPool = sync.Pool{
New: func() interface{} { return new(lexer) },
}
func Parse(gql []byte) (*Operation, error) {
var err error
if len(gql) == 0 {
return nil, errors.New("blank query")
}
l := lexPool.Get().(*lexer)
l.Reset()
defer lexPool.Put(l)
if err = lex(l, gql); err != nil {
return nil, err
}
p := &Parser{
input: l.input,
pos: -1,
items: l.items,
}
op := opPool.Get().(*Operation)
op.Reset()
op.Fields = op.fieldsA[:0]
s := -1
qf := false
for {
if p.peek(itemEOF) {
p.ignore()
break
}
if p.peek(itemFragment) {
p.ignore()
if f, err := p.parseFragment(); err != nil {
fragPool.Put(f)
return nil, err
}
} else {
if !qf && p.peek(itemQuery, itemMutation, itemSub, itemObjOpen) {
s = p.pos
qf = true
}
p.ignore()
}
}
p.reset(s)
if err := p.parseOp(op); err != nil {
return nil, err
}
for _, v := range p.frags {
fragPool.Put(v)
}
return op, nil
}
func (p *Parser) parseFragment() (*Fragment, error) {
var err error
frag := fragPool.Get().(*Fragment)
frag.Reset()
frag.Fields = frag.fieldsA[:0]
if p.peek(itemName) {
frag.Name = p.val(p.next())
} else {
return frag, errors.New("fragment: missing name")
}
if p.peek(itemOn) {
p.ignore()
} else {
return frag, errors.New("fragment: missing 'on' keyword")
}
if p.peek(itemName) {
frag.On = p.vall(p.next())
} else {
return frag, errors.New("fragment: missing table name after 'on' keyword")
}
if p.peek(itemObjOpen) {
p.ignore()
} else {
return frag, fmt.Errorf("fragment: expecting a '{', got: %s", p.next())
}
frag.Fields, err = p.parseFields(frag.Fields)
if err != nil {
return frag, fmt.Errorf("fragment: %v", err)
}
if p.frags == nil {
p.frags = make(map[uint64]*Fragment)
}
_, _ = p.h.WriteString(frag.Name)
k := p.h.Sum64()
p.h.Reset()
p.frags[k] = frag
return frag, nil
}
func (p *Parser) parseOp(op *Operation) error {
var err error
var typeSet bool
if p.peek(itemQuery, itemMutation, itemSub) {
err = p.parseOpTypeAndArgs(op)
if err != nil {
return fmt.Errorf("%s: %v", op.Type, err)
}
typeSet = true
}
if p.peek(itemObjOpen) {
p.ignore()
if !typeSet {
op.Type = opQuery
}
for {
if p.peek(itemEOF, itemFragment) {
p.ignore()
break
}
op.Fields, err = p.parseFields(op.Fields)
if err != nil {
return fmt.Errorf("%s: %v", op.Type, err)
}
}
} else {
return fmt.Errorf("expecting a query, mutation or subscription, got: %s", p.next())
}
return nil
}
func (p *Parser) parseOpTypeAndArgs(op *Operation) error {
item := p.next()
switch item._type {
case itemQuery:
op.Type = opQuery
case itemMutation:
op.Type = opMutate
case itemSub:
op.Type = opSub
}
op.Args = op.argsA[:0]
var err error
if p.peek(itemName) {
op.Name = p.val(p.next())
}
if p.peek(itemArgsOpen) {
p.ignore()
op.Args, err = p.parseOpParams(op.Args)
if err != nil {
return err
}
}
return nil
return parseSelectionSet(gql)
}
func ParseArgValue(argVal string) (*Node, error) {
@ -319,158 +123,229 @@ func ParseArgValue(argVal string) (*Node, error) {
return op, err
}
func (p *Parser) parseFields(fields []Field) ([]Field, error) {
func parseSelectionSet(gql []byte) (*Operation, error) {
var err error
st := NewStack()
if !p.peek(itemName, itemSpread) {
return nil, fmt.Errorf("unexpected token: %s", p.peekNext())
if len(gql) == 0 {
return nil, errors.New("blank query")
}
for {
if p.peek(itemEOF) {
p.ignore()
return nil, errors.New("invalid query")
l := lexPool.Get().(*lexer)
l.Reset()
if err = lex(l, gql); err != nil {
return nil, err
}
p := &Parser{
input: l.input,
pos: -1,
items: l.items,
}
var op *Operation
if p.peek(itemObjOpen) {
p.ignore()
op, err = p.parseQueryOp()
} else {
op, err = p.parseOp()
}
if err != nil {
return nil, err
}
if p.peek(itemObjClose) {
p.ignore()
} else {
return nil, fmt.Errorf("operation missing closing '}'")
}
if !p.peek(itemEOF) {
p.ignore()
return nil, fmt.Errorf("invalid '%s' found after closing '}'", p.current())
}
lexPool.Put(l)
return op, err
}
func (p *Parser) next() item {
n := p.pos + 1
if n >= len(p.items) {
p.err = errEOT
return item{_type: itemEOF}
}
p.pos = n
return p.items[p.pos]
}
func (p *Parser) ignore() {
n := p.pos + 1
if n >= len(p.items) {
p.err = errEOT
return
}
p.pos = n
}
func (p *Parser) current() string {
item := p.items[p.pos]
return b2s(p.input[item.pos:item.end])
}
func (p *Parser) peek(types ...itemType) bool {
n := p.pos + 1
// if p.items[n]._type == itemEOF {
// return false
// }
if n >= len(p.items) {
return false
}
for i := 0; i < len(types); i++ {
if p.items[n]._type == types[i] {
return true
}
}
return false
}
if p.peek(itemObjClose) {
p.ignore()
func (p *Parser) parseOp() (*Operation, error) {
if !p.peek(itemQuery, itemMutation, itemSub) {
err := errors.New("expecting a query, mutation or subscription")
return nil, err
}
item := p.next()
if st.Len() != 0 {
st.Pop()
continue
} else {
break
}
}
op := opPool.Get().(*Operation)
op.Reset()
if len(fields) >= maxFields {
return nil, fmt.Errorf("too many fields (max %d)", maxFields)
}
switch item._type {
case itemQuery:
op.Type = opQuery
case itemMutation:
op.Type = opMutate
case itemSub:
op.Type = opSub
}
isFrag := false
op.Fields = op.fieldsA[:0]
op.Args = op.argsA[:0]
if p.peek(itemSpread) {
p.ignore()
isFrag = true
}
var err error
if isFrag {
fields, err = p.parseFragmentFields(st, fields)
} else {
fields, err = p.parseNormalFields(st, fields)
}
if p.peek(itemName) {
op.Name = p.val(p.next())
}
if p.peek(itemArgsOpen) {
p.ignore()
op.Args, err = p.parseOpParams(op.Args)
if err != nil {
return nil, err
}
}
return fields, nil
}
func (p *Parser) parseNormalFields(st *Stack, fields []Field) ([]Field, error) {
if !p.peek(itemName) {
return nil, fmt.Errorf("expecting an alias or field name, got: %s", p.next())
}
fields = append(fields, Field{ID: int32(len(fields))})
f := &fields[(len(fields) - 1)]
f.Args = f.argsA[:0]
f.Children = f.childrenA[:0]
// Parse the field
if err := p.parseField(f); err != nil {
return nil, err
}
if st.Len() == 0 {
f.ParentID = -1
} else {
pid := st.Peek()
f.ParentID = pid
fields[pid].Children = append(fields[pid].Children, f.ID)
}
// The first opening curley brackets after this
// comes the columns or child fields
if p.peek(itemObjOpen) {
p.ignore()
st.Push(f.ID)
for n := 0; n < 10; n++ {
if !p.peek(itemName) {
break
}
op.Fields, err = p.parseFields(op.Fields)
if err != nil {
return nil, err
}
}
}
return fields, nil
return op, nil
}
func (p *Parser) parseFragmentFields(st *Stack, fields []Field) ([]Field, error) {
func (p *Parser) parseQueryOp() (*Operation, error) {
op := opPool.Get().(*Operation)
op.Reset()
op.Type = opQuery
op.Fields = op.fieldsA[:0]
op.Args = op.argsA[:0]
var err error
pid := st.Peek()
if p.peek(itemOn) {
p.ignore()
fields[pid].Union = true
for n := 0; n < 10; n++ {
if !p.peek(itemName) {
break
}
if fields, err = p.parseNormalFields(st, fields); err != nil {
op.Fields, err = p.parseFields(op.Fields)
if err != nil {
return nil, err
}
}
return op, nil
}
func (p *Parser) parseFields(fields []Field) ([]Field, error) {
st := util.NewStack()
for {
if len(fields) >= maxFields {
return nil, fmt.Errorf("too many fields (max %d)", maxFields)
}
if p.peek(itemEOF, itemObjClose) {
p.ignore()
st.Pop()
if st.Len() == 0 {
break
} else {
continue
}
}
if !p.peek(itemName) {
return nil, errors.New("expecting an alias or field name")
}
fields = append(fields, Field{ID: int32(len(fields))})
f := &fields[(len(fields) - 1)]
f.Args = f.argsA[:0]
f.Children = f.childrenA[:0]
// Parse the inside of the the fields () parentheses
// in short parse the args like id, where, etc
if err := p.parseField(f); err != nil {
return nil, err
}
// If parent is a union selector than copy over args from the parent
// to the first child which is the root selector for each union type.
for i := pid + 1; i < int32(len(fields)); i++ {
f := &fields[i]
if f.ParentID == pid {
f.Args = fields[pid].Args
}
intf := st.Peek()
if pid, ok := intf.(int32); ok {
f.ParentID = pid
fields[pid].Children = append(fields[pid].Children, f.ID)
} else {
f.ParentID = -1
}
} else {
if !p.peek(itemName) {
return nil, fmt.Errorf("expecting a fragment name, got: %s", p.next())
}
// The first opening curley brackets after this
// comes the columns or child fields
if p.peek(itemObjOpen) {
p.ignore()
st.Push(f.ID)
name := p.val(p.next())
_, _ = p.h.WriteString(name)
id := p.h.Sum64()
p.h.Reset()
fr, ok := p.frags[id]
if !ok {
return nil, fmt.Errorf("no fragment named '%s' defined", name)
}
ff := fr.Fields
n := int32(len(fields))
fields = append(fields, ff...)
for i := 0; i < len(ff); i++ {
k := (n + int32(i))
f := &fields[k]
f.ID = int32(k)
// If this is the top-level point the parent to the parent of the
// previous field.
if f.ParentID == -1 {
f.ParentID = pid
if f.ParentID != -1 {
fields[pid].Children = append(fields[pid].Children, f.ID)
}
// Update all the other parents id's by our new place in this new array
} else if p.peek(itemObjClose) {
if st.Len() == 0 {
break
} else {
f.ParentID += n
}
// Copy over children since fields append is not a deep copy
f.Children = make([]int32, len(f.Children))
copy(f.Children, ff[i].Children)
// Copy over args since args append is not a deep copy
f.Args = make([]Arg, len(f.Args))
copy(f.Args, ff[i].Args)
// Update all the children which is needed.
for j := range f.Children {
f.Children[j] += n
continue
}
}
}
@ -571,8 +446,10 @@ func (p *Parser) parseList() (*Node, error) {
}
if ty == 0 {
ty = node.Type
} else if ty != node.Type {
return nil, errors.New("All values in a list must be of the same type")
} else {
if ty != node.Type {
return nil, errors.New("All values in a list must be of the same type")
}
}
node.Parent = parent
nodes = append(nodes, node)
@ -669,57 +546,6 @@ func (p *Parser) vall(v item) string {
return b2s(p.input[v.pos:v.end])
}
func (p *Parser) peek(types ...itemType) bool {
n := p.pos + 1
l := len(types)
// if p.items[n]._type == itemEOF {
// return false
// }
if n >= len(p.items) {
return types[0] == itemEOF
}
if l == 1 {
return p.items[n]._type == types[0]
}
for i := 0; i < l; i++ {
if p.items[n]._type == types[i] {
return true
}
}
return false
}
func (p *Parser) next() item {
n := p.pos + 1
if n >= len(p.items) {
p.err = errEOT
return item{_type: itemEOF}
}
p.pos = n
return p.items[p.pos]
}
func (p *Parser) ignore() {
n := p.pos + 1
if n >= len(p.items) {
p.err = errEOT
return
}
p.pos = n
}
func (p *Parser) peekNext() string {
item := p.items[p.pos+1]
return b2s(p.input[item.pos:item.end])
}
func (p *Parser) reset(to int) {
p.pos = to
}
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
@ -753,9 +579,34 @@ func (t parserType) String() string {
case NodeList:
v = "node-list"
}
return v
return fmt.Sprintf("<%s>", v)
}
func FreeNode(n *Node) {
// type Frees struct {
// n *Node
// loc int
// }
// var freeList []Frees
// func FreeNode(n *Node, loc int) {
// j := -1
// for i := range freeList {
// if n == freeList[i].n {
// j = i
// break
// }
// }
// if j == -1 {
// nodePool.Put(n)
// freeList = append(freeList, Frees{n, loc})
// } else {
// fmt.Printf("(%d) RE_FREE %d %p %s %s\n", loc, freeList[j].loc, freeList[j].n, n.Name, n.Type)
// }
// }
func FreeNode(n *Node, loc int) {
nodePool.Put(n)
}

View File

@ -2,9 +2,8 @@ package qcode
import (
"errors"
"testing"
"github.com/chirino/graphql/schema"
"testing"
)
func TestCompile1(t *testing.T) {
@ -121,7 +120,7 @@ updateThread {
}
}
}
}}`
}`
qcompile, _ := NewCompiler(Config{})
_, err := qcompile.Compile([]byte(gql), "anon")
@ -131,93 +130,6 @@ updateThread {
}
func TestFragmentsCompile1(t *testing.T) {
gql := `
fragment userFields1 on user {
id
email
}
query {
users {
...userFields2
created_at
...userFields1
}
}
fragment userFields2 on user {
first_name
last_name
}
`
qcompile, _ := NewCompiler(Config{})
_, err := qcompile.Compile([]byte(gql), "user")
if err != nil {
t.Fatal(err)
}
}
func TestFragmentsCompile2(t *testing.T) {
gql := `
query {
users {
...userFields2
created_at
...userFields1
}
}
fragment userFields1 on user {
id
email
}
fragment userFields2 on user {
first_name
last_name
}`
qcompile, _ := NewCompiler(Config{})
_, err := qcompile.Compile([]byte(gql), "user")
if err != nil {
t.Fatal(err)
}
}
func TestFragmentsCompile3(t *testing.T) {
gql := `
fragment userFields1 on user {
id
email
}
fragment userFields2 on user {
first_name
last_name
}
query {
users {
...userFields2
created_at
...userFields1
}
}
`
qcompile, _ := NewCompiler(Config{})
_, err := qcompile.Compile([]byte(gql), "user")
if err != nil {
t.Fatal(err)
}
}
var gql = []byte(`
{products(
# returns only 30 items
@ -239,29 +151,6 @@ var gql = []byte(`
price
}}`)
var gqlWithFragments = []byte(`
fragment userFields1 on user {
id
email
__typename
}
query {
users {
...userFields2
created_at
...userFields1
__typename
}
}
fragment userFields2 on user {
first_name
last_name
__typename
}`)
func BenchmarkQCompile(b *testing.B) {
qcompile, _ := NewCompiler(Config{})
@ -294,22 +183,8 @@ func BenchmarkQCompileP(b *testing.B) {
})
}
func BenchmarkQCompileFragment(b *testing.B) {
qcompile, _ := NewCompiler(Config{})
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
_, err := qcompile.Compile(gqlWithFragments, "user")
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkParse(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
@ -336,18 +211,6 @@ func BenchmarkParseP(b *testing.B) {
})
}
func BenchmarkParseFragment(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
_, err := Parse(gqlWithFragments)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkSchemaParse(b *testing.B) {
b.ResetTimer()

View File

@ -12,7 +12,6 @@ import (
)
type QType int
type SType int
type Action int
const (
@ -20,8 +19,7 @@ const (
)
const (
QTUnknown QType = iota
QTQuery
QTQuery QType = iota + 1
QTMutation
QTInsert
QTUpdate
@ -29,12 +27,6 @@ const (
QTUpsert
)
const (
STNone SType = iota
STUnion
STMember
)
type QCode struct {
Type QType
ActionVar string
@ -46,8 +38,6 @@ type QCode struct {
type Select struct {
ID int32
ParentID int32
UParentID int32
Type SType
Args map[string]*Node
Name string
FieldName string
@ -287,7 +277,6 @@ func (com *Compiler) Compile(query []byte, role string) (*QCode, error) {
return nil, err
}
freeNodes(op)
opPool.Put(op)
return &qc, nil
@ -382,11 +371,7 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
})
s := &selects[(len(selects) - 1)]
if field.Union {
s.Type = STUnion
}
if field.Alias != "" {
if len(field.Alias) != 0 {
s.FieldName = field.Alias
} else {
s.FieldName = s.Name
@ -397,11 +382,6 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
} else {
p := &selects[s.ParentID]
p.Children = append(p.Children, s.ID)
if p.Type == STUnion {
s.Type = STMember
s.UParentID = p.ParentID
}
}
if skipRender {
@ -439,7 +419,6 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
com.AddFilters(qc, s, role)
s.Cols = make([]Column, 0, len(field.Children))
cm := make(map[string]struct{})
action = QTQuery
for _, cid := range field.Children {
@ -449,27 +428,19 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
continue
}
var fname string
if f.Alias != "" {
fname = f.Alias
} else {
fname = f.Name
}
if _, ok := cm[fname]; ok {
continue
} else {
cm[fname] = struct{}{}
}
if len(f.Children) != 0 {
val := f.ID | (s.ID << 16)
st.Push(val)
continue
}
col := Column{Name: f.Name, FieldName: fname}
col := Column{Name: f.Name}
if len(f.Alias) != 0 {
col.FieldName = f.Alias
} else {
col.FieldName = f.Name
}
s.Cols = append(s.Cols, col)
}
@ -481,7 +452,6 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
}
qc.Selects = selects[:id]
return nil
}
@ -513,42 +483,50 @@ func (com *Compiler) AddFilters(qc *QCode, sel *Select, role string) {
func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg, role string) error {
var err error
// don't free this arg either previously done or will be free'd
// in the future like in psql
var df bool
for i := range args {
arg := &args[i]
switch arg.Name {
case "id":
err = com.compileArgID(sel, arg)
err, df = com.compileArgID(sel, arg)
case "search":
err = com.compileArgSearch(sel, arg)
err, df = com.compileArgSearch(sel, arg)
case "where":
err = com.compileArgWhere(sel, arg, role)
err, df = com.compileArgWhere(sel, arg, role)
case "orderby", "order_by", "order":
err = com.compileArgOrderBy(sel, arg)
err, df = com.compileArgOrderBy(sel, arg)
case "distinct_on", "distinct":
err = com.compileArgDistinctOn(sel, arg)
err, df = com.compileArgDistinctOn(sel, arg)
case "limit":
err = com.compileArgLimit(sel, arg)
err, df = com.compileArgLimit(sel, arg)
case "offset":
err = com.compileArgOffset(sel, arg)
err, df = com.compileArgOffset(sel, arg)
case "first":
err = com.compileArgFirstLast(sel, arg, PtForward)
err, df = com.compileArgFirstLast(sel, arg, PtForward)
case "last":
err = com.compileArgFirstLast(sel, arg, PtBackward)
err, df = com.compileArgFirstLast(sel, arg, PtBackward)
case "after":
err = com.compileArgAfterBefore(sel, arg, PtForward)
err, df = com.compileArgAfterBefore(sel, arg, PtForward)
case "before":
err = com.compileArgAfterBefore(sel, arg, PtBackward)
err, df = com.compileArgAfterBefore(sel, arg, PtBackward)
}
if !df {
FreeNode(arg.Val, 5)
}
if err != nil {
@ -629,12 +607,14 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
}
// Objects inside a list
if node.Name == "" {
if len(node.Name) == 0 {
pushChildren(st, node.exp, node)
continue
} else if _, ok := com.bl[node.Name]; ok {
continue
} else {
if _, ok := com.bl[node.Name]; ok {
continue
}
}
ex, err := newExp(st, node, usePool)
@ -657,20 +637,39 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
}
}
if usePool {
st.Push(node)
for {
if st.Len() == 0 {
break
}
intf := st.Pop()
node, ok := intf.(*Node)
if !ok || node == nil {
continue
}
for i := range node.Children {
st.Push(node.Children[i])
}
FreeNode(node, 1)
}
}
return root, needsUser, nil
}
func (com *Compiler) compileArgID(sel *Select, arg *Arg) error {
func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) {
if sel.ID != 0 {
return nil
return nil, false
}
if sel.Where != nil && sel.Where.Op == OpEqID {
return nil
return nil, false
}
if arg.Val.Type != NodeVar {
return argErr("id", "variable")
return argErr("id", "variable"), false
}
ex := expPool.Get().(*Exp)
@ -681,12 +680,12 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) error {
ex.Val = arg.Val.Val
sel.Where = ex
return nil
return nil, false
}
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) error {
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) {
if arg.Val.Type != NodeVar {
return argErr("search", "variable")
return argErr("search", "variable"), false
}
ex := expPool.Get().(*Exp)
@ -701,19 +700,18 @@ func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) error {
}
sel.Args[arg.Name] = arg.Val
arg.df = true
AddFilter(sel, ex)
return nil
return nil, true
}
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) error {
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) (error, bool) {
st := util.NewStack()
var err error
ex, nu, err := com.compileArgObj(st, arg)
if err != nil {
return err
return err, false
}
if nu && role == "anon" {
@ -721,12 +719,12 @@ func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) error {
}
AddFilter(sel, ex)
return nil
return nil, true
}
func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error {
func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
if arg.Val.Type != NodeObj {
return fmt.Errorf("expecting an object")
return fmt.Errorf("expecting an object"), false
}
st := util.NewStack()
@ -744,15 +742,16 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error {
node, ok := intf.(*Node)
if !ok || node == nil {
return fmt.Errorf("17: unexpected value %v (%t)", intf, intf)
return fmt.Errorf("17: unexpected value %v (%t)", intf, intf), false
}
if _, ok := com.bl[node.Name]; ok {
FreeNode(node, 2)
continue
}
if node.Type != NodeStr && node.Type != NodeVar {
return fmt.Errorf("expecting a string or variable")
return fmt.Errorf("expecting a string or variable"), false
}
ob := &OrderBy{}
@ -771,24 +770,25 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error {
case "desc_nulls_last":
ob.Order = OrderDescNullsLast
default:
return fmt.Errorf("valid values include asc, desc, asc_nulls_first and desc_nulls_first")
return fmt.Errorf("valid values include asc, desc, asc_nulls_first and desc_nulls_first"), false
}
setOrderByColName(ob, node)
sel.OrderBy = append(sel.OrderBy, ob)
FreeNode(node, 3)
}
return nil
return nil, false
}
func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) error {
func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) (error, bool) {
node := arg.Val
if _, ok := com.bl[node.Name]; ok {
return nil
return nil, false
}
if node.Type != NodeList && node.Type != NodeStr {
return fmt.Errorf("expecting a list of strings or just a string")
return fmt.Errorf("expecting a list of strings or just a string"), false
}
if node.Type == NodeStr {
@ -797,57 +797,58 @@ func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) error {
for i := range node.Children {
sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val)
FreeNode(node.Children[i], 5)
}
return nil
return nil, false
}
func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) error {
func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) (error, bool) {
node := arg.Val
if node.Type != NodeInt {
return argErr("limit", "number")
return argErr("limit", "number"), false
}
sel.Paging.Limit = node.Val
return nil
return nil, false
}
func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) error {
func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) {
node := arg.Val
if node.Type != NodeVar {
return argErr("offset", "variable")
return argErr("offset", "variable"), false
}
sel.Paging.Offset = node.Val
return nil
return nil, false
}
func (com *Compiler) compileArgFirstLast(sel *Select, arg *Arg, pt PagingType) error {
func (com *Compiler) compileArgFirstLast(sel *Select, arg *Arg, pt PagingType) (error, bool) {
node := arg.Val
if node.Type != NodeInt {
return argErr(arg.Name, "number")
return argErr(arg.Name, "number"), false
}
sel.Paging.Type = pt
sel.Paging.Limit = node.Val
return nil
return nil, false
}
func (com *Compiler) compileArgAfterBefore(sel *Select, arg *Arg, pt PagingType) error {
func (com *Compiler) compileArgAfterBefore(sel *Select, arg *Arg, pt PagingType) (error, bool) {
node := arg.Val
if node.Type != NodeVar || node.Val != "cursor" {
return fmt.Errorf("value for argument '%s' must be a variable named $cursor", arg.Name)
return fmt.Errorf("value for argument '%s' must be a variable named $cursor", arg.Name), false
}
sel.Paging.Type = pt
sel.Paging.Cursor = true
return nil
return nil, false
}
// var zeroTrv = &trval{}
@ -1048,7 +1049,7 @@ func setWhereColName(ex *Exp, node *Node) {
if n.Type != NodeObj {
continue
}
if n.Name != "" {
if len(n.Name) != 0 {
k := n.Name
if k == "and" || k == "or" || k == "not" ||
k == "_and" || k == "_or" || k == "_not" {
@ -1227,81 +1228,3 @@ func FreeExp(ex *Exp) {
func argErr(name, ty string) error {
return fmt.Errorf("value for argument '%s' must be a %s", name, ty)
}
func freeNodes(op *Operation) {
var st *util.Stack
fm := make(map[*Node]struct{})
for i := range op.Args {
arg := op.Args[i]
if arg.df {
continue
}
for i := range arg.Val.Children {
if st == nil {
st = util.NewStack()
}
c := arg.Val.Children[i]
if _, ok := fm[c]; !ok {
st.Push(c)
}
}
if _, ok := fm[arg.Val]; !ok {
nodePool.Put(arg.Val)
fm[arg.Val] = struct{}{}
}
}
for i := range op.Fields {
f := op.Fields[i]
for j := range f.Args {
arg := f.Args[j]
if arg.df {
continue
}
for k := range arg.Val.Children {
if st == nil {
st = util.NewStack()
}
c := arg.Val.Children[k]
if _, ok := fm[c]; !ok {
st.Push(c)
}
}
if _, ok := fm[arg.Val]; !ok {
nodePool.Put(arg.Val)
fm[arg.Val] = struct{}{}
}
}
}
if st == nil {
return
}
for {
if st.Len() == 0 {
break
}
intf := st.Pop()
node, ok := intf.(*Node)
if !ok || node == nil {
continue
}
for i := range node.Children {
st.Push(node.Children[i])
}
if _, ok := fm[node]; !ok {
nodePool.Put(node)
fm[node] = struct{}{}
}
}
}

View File

@ -29,8 +29,6 @@ func al(b byte) bool {
func (qt QType) String() string {
switch qt {
case QTUnknown:
return "unknown"
case QTQuery:
return "query"
case QTMutation:

View File

@ -28,18 +28,17 @@ func (sg *SuperGraph) prepare(q *query, role string) {
var err error
qb := []byte(q.ai.Query)
vars := []byte(q.ai.Vars)
switch q.qt {
case qcode.QTQuery:
if sg.abacEnabled {
stmts, err = sg.buildMultiStmt(qb, vars)
stmts, err = sg.buildMultiStmt(qb, q.ai.Vars)
} else {
stmts, err = sg.buildRoleStmt(qb, vars, role)
stmts, err = sg.buildRoleStmt(qb, q.ai.Vars, role)
}
case qcode.QTMutation:
stmts, err = sg.buildRoleStmt(qb, vars, role)
stmts, err = sg.buildRoleStmt(qb, q.ai.Vars, role)
}
if err != nil {
@ -75,20 +74,25 @@ func (sg *SuperGraph) initPrepared() error {
h.SetSeed(sg.hashSeed)
for _, v := range list {
if v.Query == "" {
if len(v.Query) == 0 {
continue
}
q := &query{ai: v, qt: qcode.GetQType(v.Query)}
qt := qcode.GetQType(v.Query)
switch qt {
switch q.qt {
case qcode.QTQuery:
sg.queries[queryID(&h, v.Name, "user")] = &query{ai: v, qt: qt}
sg.queries[queryID(&h, v.Name, "anon")] = &query{ai: v, qt: qt}
sg.queries[queryID(&h, v.Name, "user")] = q
h.Reset()
if sg.anonExists {
sg.queries[queryID(&h, v.Name, "anon")] = q
h.Reset()
}
case qcode.QTMutation:
for _, role := range sg.conf.Roles {
sg.queries[queryID(&h, v.Name, role.Name)] = &query{ai: v, qt: qt}
sg.queries[queryID(&h, v.Name, role.Name)] = q
h.Reset()
}
}
}
@ -113,7 +117,7 @@ func (sg *SuperGraph) prepareRoleStmt() error {
io.WriteString(w, `(SELECT (CASE`)
for _, role := range sg.conf.Roles {
if role.Match == "" {
if len(role.Match) == 0 {
continue
}
io.WriteString(w, ` WHEN `)
@ -124,7 +128,7 @@ func (sg *SuperGraph) prepareRoleStmt() error {
}
io.WriteString(w, ` ELSE $2 END) FROM (`)
io.WriteString(w, rq)
io.WriteString(w, sg.conf.RolesQuery)
io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) `)
io.WriteString(w, `ELSE 'anon' END) FROM (VALUES (1)) AS "_sg_auth_filler" LIMIT 1; `)
@ -159,11 +163,8 @@ func (sg *SuperGraph) initAllowList() error {
}
// nolint: errcheck
func queryID(h *maphash.Hash, name, role string) uint64 {
func queryID(h *maphash.Hash, name string, role string) uint64 {
h.WriteString(name)
h.WriteString(role)
v := h.Sum64()
h.Reset()
return v
return h.Sum64()
}

View File

@ -4,10 +4,10 @@ import (
"bytes"
"errors"
"fmt"
"hash/maphash"
"net/http"
"sync"
"github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/core/internal/qcode"
"github.com/dosco/super-graph/jsn"
)
@ -16,13 +16,12 @@ func (sg *SuperGraph) execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([]
var err error
sel := st.qc.Selects
h := maphash.Hash{}
h.SetSeed(sg.hashSeed)
h := xxhash.New()
// fetch the field name used within the db response json
// that are used to mark insertion points and the mapping between
// those field names and their select objects
fids, sfmap := sg.parentFieldIds(&h, sel, st.md.Skipped())
fids, sfmap := sg.parentFieldIds(h, sel, st.md.Skipped)
// fetch the field values of the marked insertion points
// these values contain the id to be used with fetching remote data
@ -31,10 +30,10 @@ func (sg *SuperGraph) execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([]
switch {
case len(from) == 1:
to, err = sg.resolveRemote(hdr, &h, from[0], sel, sfmap)
to, err = sg.resolveRemote(hdr, h, from[0], sel, sfmap)
case len(from) > 1:
to, err = sg.resolveRemotes(hdr, &h, from, sel, sfmap)
to, err = sg.resolveRemotes(hdr, h, from, sel, sfmap)
default:
return nil, errors.New("something wrong no remote ids found in db response")
@ -56,7 +55,7 @@ func (sg *SuperGraph) execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([]
func (sg *SuperGraph) resolveRemote(
hdr http.Header,
h *maphash.Hash,
h *xxhash.Digest,
field jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
@ -67,8 +66,7 @@ func (sg *SuperGraph) resolveRemote(
to := toA[:1]
// use the json key to find the related Select object
_, _ = h.Write(field.Key)
k1 := h.Sum64()
k1 := xxhash.Sum64(field.Key)
s, ok := sfmap[k1]
if !ok {
@ -119,7 +117,7 @@ func (sg *SuperGraph) resolveRemote(
func (sg *SuperGraph) resolveRemotes(
hdr http.Header,
h *maphash.Hash,
h *xxhash.Digest,
from []jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
@ -136,8 +134,7 @@ func (sg *SuperGraph) resolveRemotes(
for i, id := range from {
// use the json key to find the related Select object
_, _ = h.Write(id.Key)
k1 := h.Sum64()
k1 := xxhash.Sum64(id.Key)
s, ok := sfmap[k1]
if !ok {
@ -195,7 +192,7 @@ func (sg *SuperGraph) resolveRemotes(
return to, cerr
}
func (sg *SuperGraph) parentFieldIds(h *maphash.Hash, sel []qcode.Select, skipped uint32) (
func (sg *SuperGraph) parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) (
[][]byte,
map[uint64]*qcode.Select) {
@ -230,15 +227,15 @@ func (sg *SuperGraph) parentFieldIds(h *maphash.Hash, sel []qcode.Select, skippe
fm[n] = r.IDField
n++
_, _ = h.Write(r.IDField)
sm[h.Sum64()] = s
k := xxhash.Sum64(r.IDField)
sm[k] = s
}
}
return fm, sm
}
func isSkipped(n, pos uint32) bool {
func isSkipped(n uint32, pos uint32) bool {
return ((n & (1 << pos)) != 0)
}

View File

@ -2,11 +2,11 @@ package core
import (
"fmt"
"hash/maphash"
"io/ioutil"
"net/http"
"strings"
"github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/core/internal/psql"
"github.com/dosco/super-graph/jsn"
)
@ -19,7 +19,7 @@ type resolvFn struct {
func (sg *SuperGraph) initResolvers() error {
var err error
sg.rmap = make(map[uint64]resolvFn)
sg.rmap = make(map[uint64]*resolvFn)
for _, t := range sg.conf.Tables {
err = sg.initRemotes(t)
@ -36,8 +36,7 @@ func (sg *SuperGraph) initResolvers() error {
}
func (sg *SuperGraph) initRemotes(t Table) error {
h := maphash.Hash{}
h.SetSeed(sg.hashSeed)
h := xxhash.New()
for _, r := range t.Remotes {
// defines the table column to be used as an id in the
@ -46,7 +45,7 @@ func (sg *SuperGraph) initRemotes(t Table) error {
// if no table column specified in the config then
// use the primary key of the table as the id
if idcol == "" {
if len(idcol) == 0 {
pcol, err := sg.pc.IDColumn(t.Name)
if err != nil {
return err
@ -76,18 +75,17 @@ func (sg *SuperGraph) initRemotes(t Table) error {
path = append(path, []byte(p))
}
rf := resolvFn{
rf := &resolvFn{
IDField: []byte(idk),
Path: path,
Fn: fn,
}
// index resolver obj by parent and child names
sg.rmap[mkkey(&h, r.Name, t.Name)] = rf
sg.rmap[mkkey(h, r.Name, t.Name)] = rf
// index resolver obj by IDField
_, _ = h.Write(rf.IDField)
sg.rmap[h.Sum64()] = rf
sg.rmap[xxhash.Sum64(rf.IDField)] = rf
}
return nil

View File

@ -1,9 +1,11 @@
package core
import "hash/maphash"
import (
"github.com/cespare/xxhash/v2"
)
// nolint: errcheck
func mkkey(h *maphash.Hash, k1, k2 string) uint64 {
func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 {
h.WriteString(k1)
h.WriteString(k2)
v := h.Sum64()

View File

@ -55,30 +55,6 @@ query {
}
```
### Fragments
Fragments make it easy to build large complex queries with small composible and re-usable fragment blocks.
```graphql
query {
users {
...userFields2
...userFields1
picture_url
}
}
fragment userFields1 on user {
id
email
}
fragment userFields2 on user {
first_name
last_name
}
```
### Sorting
To sort or ordering results just use the `order_by` argument. This can be combined with `where`, `search`, etc to build complex queries to fit you needs.

View File

@ -4,8 +4,6 @@ title: Introduction
sidebar_label: Introduction
---
import useBaseUrl from '@docusaurus/useBaseUrl'; // Add to the top of the file below the front matter.
Super Graph is a service that instantly and without code gives you a high performance and secure GraphQL API. Your GraphQL queries are auto translated into a single fast SQL query. No more spending weeks or months writing backend API code. Just make the query you need and Super Graph will do the rest.
Super Graph has a rich feature set like integrating with your existing Ruby on Rails apps, joining your DB with data from remote APIs, Role and Attribute based access control, Support for JWT tokens, DB migrations, seeding and a lot more.
@ -136,9 +134,3 @@ mutation {
}
}
```
### Built-in GraphQL Editor
Quickly craft and test your queries with a full-featured GraphQL editor. Auto-complete and schema documentation is automatically available.
<img alt="Zipkin Traces" src={useBaseUrl("img/webui.jpg")} />

View File

@ -95,7 +95,7 @@ auth:
type: jwt
jwt:
# valid providers are auth0, firebase and none
# the two providers are 'auth0' and 'none'
provider: auth0
secret: abc335bfcfdb04e50db5bb0a4d67ab9
public_key_file: /secrets/public_key.pem
@ -108,19 +108,6 @@ We can get the JWT token either from the `authorization` header where we expect
For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
### Firebase Auth
```yaml
auth:
type: jwt
jwt:
provider: firebase
audience: <firebase-project-id>
```
Firebase auth also uses JWT the keys are auto-fetched from Google and used according to their documentation mechanism. The `audience` config value needs to be set to your project id and everything else is taken care for you.
### HTTP Headers
```yaml

View File

@ -1,13 +0,0 @@
---
id: webui
title: Web UI / GraphQL Editor
sidebar_label: Web UI
---
import useBaseUrl from '@docusaurus/useBaseUrl'; // Add to the top of the file below the front matter.
<img alt="Zipkin Traces" src={useBaseUrl("img/webui.jpg")} />
Super Graph comes with a build-in GraphQL editor that only runs in development. Use it to craft your queries and copy-paste them into you're app once you're ready. The editor supports auto-completation and schema documentation. This makes it super easy to craft and test your query all in one go without knowing anything about the underlying database structure.
You can even set query variables or http headers as required. To simulate an authenticated user set the http header `"X-USER-ID": 5` to the user id of the user you want to test with.

View File

@ -36,8 +36,8 @@ module.exports = {
position: "left",
},
{
label: "AbtCode",
href: "https://abtcode.com/s/super-graph",
label: "Art Compute",
href: "https://artcompute.com/s/super-graph",
position: "left",
},
],

View File

@ -3,7 +3,6 @@ module.exports = {
Docusaurus: [
"home",
"intro",
"webui",
"start",
"why",
"graphql",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

3
go.mod
View File

@ -12,11 +12,13 @@ require (
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
github.com/brianvoe/gofakeit/v5 v5.2.0
github.com/cespare/xxhash/v2 v2.1.1
github.com/chirino/graphql v0.0.0-20200430165312-293648399b1a
github.com/daaku/go.zipexe v1.0.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dlclark/regexp2 v1.2.0 // indirect
github.com/dop251/goja v0.0.0-20200424152103-d0b8fda54cd0
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/garyburd/redigo v1.6.0
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
@ -28,6 +30,7 @@ require (
github.com/openzipkin/zipkin-go v0.2.2
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/common v0.4.0
github.com/rs/cors v1.7.0
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect

4
go.sum
View File

@ -55,6 +55,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chirino/graphql v0.0.0-20200430165312-293648399b1a h1:WVu7r2vwlrBVmunbSSU+9/3M3AgsQyhE49CKDjHiFq4=
github.com/chirino/graphql v0.0.0-20200430165312-293648399b1a/go.mod h1:wQjjxFMFyMlsWh4Z3nMuHQtevD4Ul9UVQSnz1JOLuP8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@ -85,6 +87,8 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20200424152103-d0b8fda54cd0 h1:EfFAcaAwGai/wlDCWwIObHBm3T2C2CCPX/SaS0fpOJ4=
github.com/dop251/goja v0.0.0-20200424152103-d0b8fda54cd0/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813 h1:NgO45/5mBLRVfiXerEFzH6ikcZ7DNRPS639xFg3ENzU=
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=

View File

@ -155,7 +155,7 @@ func cmdVersion(cmd *cobra.Command, args []string) {
}
func BuildDetails() string {
if version == "" {
if len(version) == 0 {
return `
Super Graph (unknown version)
For documentation, visit https://supergraph.dev

View File

@ -88,10 +88,6 @@ func cmdNew(cmd *cobra.Command, args []string) {
}
})
ifNotExists(path.Join(appConfigPath, "allow.list"), func(p string) error {
return ioutil.WriteFile(p, []byte{}, 0644)
})
// Create app migrations folder and add relevant files
appMigrationsPath := path.Join(appConfigPath, "migrations")

View File

@ -80,8 +80,10 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
func graphQLFunc(sg *core.SuperGraph, query string, data interface{}, opt map[string]string) map[string]interface{} {
ct := context.Background()
if v, ok := opt["user_id"]; ok && v != "" {
if v, ok := opt["user_id"]; ok && len(v) != 0 {
ct = context.WithValue(ct, core.UserIDKey, v)
} else {
ct = context.WithValue(ct, core.UserIDKey, "-1")
}
// var role string
@ -144,7 +146,7 @@ func (c *csvSource) Values() ([]interface{}, error) {
for _, v := range c.rows[c.i] {
switch {
case v == "":
case len(v) == 0:
vals = append(vals, "")
case isDigit(v):
var n int
@ -243,7 +245,7 @@ func avatarURL(size int) string {
return fmt.Sprintf("https://i.pravatar.cc/%d?%d", size, rand.Intn(5000))
}
func imageURL(width, height int) string {
func imageURL(width int, height int) string {
return fmt.Sprintf("https://picsum.photos/%d/%d?%d", width, height, rand.Intn(5000))
}

View File

@ -66,7 +66,7 @@ func newViper(configPath, configFile string) *viper.Viper {
vi.SetDefault("host_port", "0.0.0.0:8080")
vi.SetDefault("web_ui", false)
vi.SetDefault("enable_tracing", false)
vi.SetDefault("auth_fail_block", false)
vi.SetDefault("auth_fail_block", "always")
vi.SetDefault("seed_file", "seed.js")
vi.SetDefault("default_block", true)
@ -90,7 +90,7 @@ func newViper(configPath, configFile string) *viper.Viper {
}
func GetConfigName() string {
if os.Getenv("GO_ENV") == "" {
if len(os.Getenv("GO_ENV")) == 0 {
return "dev"
}

View File

@ -105,7 +105,7 @@ func apiV1(w http.ResponseWriter, r *http.Request) {
}
if err == nil {
if conf.CacheControl != "" && res.Operation() == core.OpQuery {
if len(conf.CacheControl) != 0 && res.Operation() == core.OpQuery {
w.Header().Set("Cache-Control", conf.CacheControl)
}
//nolint: errcheck

View File

@ -32,7 +32,6 @@ type Auth struct {
Secret string
PubKeyFile string `mapstructure:"public_key_file"`
PubKeyType string `mapstructure:"public_key_type"`
Audience string `mapstructure:"audience"`
}
Header struct {
@ -47,17 +46,17 @@ func SimpleHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
ctx := r.Context()
userIDProvider := r.Header.Get("X-User-ID-Provider")
if userIDProvider != "" {
if len(userIDProvider) != 0 {
ctx = context.WithValue(ctx, core.UserIDProviderKey, userIDProvider)
}
userID := r.Header.Get("X-User-ID")
if userID != "" {
if len(userID) != 0 {
ctx = context.WithValue(ctx, core.UserIDKey, userID)
}
userRole := r.Header.Get("X-User-Role")
if userRole != "" {
if len(userRole) != 0 {
ctx = context.WithValue(ctx, core.UserRoleKey, userRole)
}
@ -68,11 +67,11 @@ func SimpleHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
func HeaderHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
hdr := ac.Header
if hdr.Name == "" {
if len(hdr.Name) == 0 {
return nil, fmt.Errorf("auth '%s': no header.name defined", ac.Name)
}
if !hdr.Exists && hdr.Value == "" {
if !hdr.Exists && len(hdr.Value) == 0 {
return nil, fmt.Errorf("auth '%s': no header.value defined", ac.Name)
}
@ -82,7 +81,7 @@ func HeaderHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
switch {
case hdr.Exists:
fo1 = (value == "")
fo1 = (len(value) == 0)
default:
fo1 = (value != hdr.Value)

View File

@ -2,32 +2,19 @@ package auth
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/dosco/super-graph/core"
)
const (
authHeader = "Authorization"
jwtAuth0 int = iota + 1
jwtFirebase int = iota + 2
firebasePKEndpoint = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
firebaseIssuerPrefix = "https://securetoken.google.com/"
authHeader = "Authorization"
jwtAuth0 int = iota + 1
)
type firebasePKCache struct {
PublicKeys map[string]string
Expiration time.Time
}
var firebasePublicKeys firebasePKCache
func JwtHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
var key interface{}
var jwtProvider int
@ -36,18 +23,16 @@ func JwtHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
if ac.JWT.Provider == "auth0" {
jwtProvider = jwtAuth0
} else if ac.JWT.Provider == "firebase" {
jwtProvider = jwtFirebase
}
secret := ac.JWT.Secret
publicKeyFile := ac.JWT.PubKeyFile
switch {
case secret != "":
case len(secret) != 0:
key = []byte(secret)
case publicKeyFile != "":
case len(publicKeyFile) != 0:
kd, err := ioutil.ReadFile(publicKeyFile)
if err != nil {
return nil, err
@ -71,10 +56,9 @@ func JwtHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
}
return func(w http.ResponseWriter, r *http.Request) {
var tok string
if cookie != "" {
if len(cookie) != 0 {
ck, err := r.Cookie(cookie)
if err != nil {
next.ServeHTTP(w, r)
@ -90,16 +74,9 @@ func JwtHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
tok = ah[7:]
}
var keyFunc jwt.Keyfunc
if jwtProvider == jwtFirebase {
keyFunc = firebaseKeyFunction
} else {
keyFunc = func(token *jwt.Token) (interface{}, error) {
return key, nil
}
}
token, err := jwt.ParseWithClaims(tok, &jwt.StandardClaims{}, keyFunc)
token, err := jwt.ParseWithClaims(tok, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return key, nil
})
if err != nil {
next.ServeHTTP(w, r)
@ -109,20 +86,12 @@ func JwtHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
if claims, ok := token.Claims.(*jwt.StandardClaims); ok {
ctx := r.Context()
if ac.JWT.Audience != "" && claims.Audience != ac.JWT.Audience {
next.ServeHTTP(w, r)
return
}
if jwtProvider == jwtAuth0 {
sub := strings.Split(claims.Subject, "|")
if len(sub) != 2 {
ctx = context.WithValue(ctx, core.UserIDProviderKey, sub[0])
ctx = context.WithValue(ctx, core.UserIDKey, sub[1])
}
} else if jwtProvider == jwtFirebase &&
claims.Issuer == firebaseIssuerPrefix+ac.JWT.Audience {
ctx = context.WithValue(ctx, core.UserIDKey, claims.Subject)
} else {
ctx = context.WithValue(ctx, core.UserIDKey, claims.Subject)
}
@ -134,92 +103,3 @@ func JwtHandler(ac *Auth, next http.Handler) (http.HandlerFunc, error) {
next.ServeHTTP(w, r)
}, nil
}
type firebaseKeyError struct {
Err error
Message string
}
func (e *firebaseKeyError) Error() string {
return e.Message + " " + e.Err.Error()
}
func firebaseKeyFunction(token *jwt.Token) (interface{}, error) {
kid, ok := token.Header["kid"]
if !ok {
return nil, &firebaseKeyError{
Message: "Error 'kid' header not found in token",
}
}
if firebasePublicKeys.Expiration.Before(time.Now()) {
resp, err := http.Get(firebasePKEndpoint)
if err != nil {
return nil, &firebaseKeyError{
Message: "Error connecting to firebase certificate server",
Err: err,
}
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &firebaseKeyError{
Message: "Error reading firebase certificate server response",
Err: err,
}
}
cachePolicy := resp.Header.Get("cache-control")
ageIndex := strings.Index(cachePolicy, "max-age=")
if ageIndex < 0 {
return nil, &firebaseKeyError{
Message: "Error parsing cache-control header: 'max-age=' not found",
}
}
ageToEnd := cachePolicy[ageIndex+8:]
endIndex := strings.Index(ageToEnd, ",")
if endIndex < 0 {
endIndex = len(ageToEnd) - 1
}
ageString := ageToEnd[:endIndex]
age, err := strconv.ParseInt(ageString, 10, 64)
if err != nil {
return nil, &firebaseKeyError{
Message: "Error parsing max-age cache policy",
Err: err,
}
}
expiration := time.Now().Add(time.Duration(time.Duration(age) * time.Second))
err = json.Unmarshal(data, &firebasePublicKeys.PublicKeys)
if err != nil {
firebasePublicKeys = firebasePKCache{}
return nil, &firebaseKeyError{
Message: "Error unmarshalling firebase public key json",
Err: err,
}
}
firebasePublicKeys.Expiration = expiration
}
if key, found := firebasePublicKeys.PublicKeys[kid.(string)]; found {
k, err := jwt.ParseRSAPublicKeyFromPEM([]byte(key))
return k, err
}
return nil, &firebaseKeyError{
Message: "Error no matching public key for kid supplied in jwt",
}
}

View File

@ -165,7 +165,7 @@ func railsAuth(ac *Auth) (*rails.Auth, error) {
}
version := ac.Rails.Version
if version == "" {
if len(version) == 0 {
return nil, errors.New("no auth.rails.version defined")
}

View File

@ -199,7 +199,7 @@ func (m *Migrator) LoadMigrations(path string) error {
for _, v := range strings.Split(upSQL, "\n") {
// Only account for regular single line comment, empty line and space/comment combination
cleanString := strings.TrimSpace(v)
if cleanString != "" &&
if len(cleanString) != 0 &&
!strings.HasPrefix(cleanString, "--") {
containsSQL = true
break

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@ func initWatcher() {
}
var d dir
if cpath == "" || cpath == "./" {
if len(cpath) == 0 || cpath == "./" {
d = Dir("./config", ReExec)
} else {
d = Dir(cpath, ReExec)
@ -52,11 +52,11 @@ func startHTTP() {
hp := strings.SplitN(conf.HostPort, ":", 2)
if len(hp) == 2 {
if conf.Host != "" {
if len(conf.Host) != 0 {
hp[0] = conf.Host
}
if conf.Port != "" {
if len(conf.Port) != 0 {
hp[1] = conf.Port
}
@ -64,7 +64,7 @@ func startHTTP() {
}
}
if conf.hostPort == "" {
if len(conf.hostPort) == 0 {
conf.hostPort = defaultHP
}
@ -123,7 +123,7 @@ func routeHandler() (http.Handler, error) {
return mux, nil
}
if conf.APIPath != "" {
if len(conf.APIPath) != 0 {
apiRoute = path.Join("/", conf.APIPath, "/v1/graphql")
}

View File

@ -134,7 +134,7 @@ database:
type: postgres
host: db
port: 5432
dbname: {{ .AppNameSlug -}}_development
dbname: {{- .AppNameSlug -}}_development
user: postgres
password: postgres

View File

@ -82,7 +82,7 @@ database:
type: postgres
host: db
port: 5432
dbname: {{ .AppNameSlug -}}_production
dbname: {{- .AppNameSlug -}}_production
user: postgres
password: postgres
#pool_size: 10

View File

@ -1,13 +0,0 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/jsn
BenchmarkGet
BenchmarkGet-16 13898 85293 ns/op 3328 B/op 2 allocs/op
BenchmarkFilter
BenchmarkFilter-16 189328 6341 ns/op 448 B/op 1 allocs/op
BenchmarkStrip
BenchmarkStrip-16 219765 5543 ns/op 224 B/op 1 allocs/op
BenchmarkReplace
BenchmarkReplace-16 100899 12022 ns/op 416 B/op 1 allocs/op
PASS
ok github.com/dosco/super-graph/jsn 6.029s

View File

@ -2,19 +2,17 @@ package jsn
import (
"bytes"
"hash/maphash"
"github.com/cespare/xxhash/v2"
)
// Filter function filters the JSON keeping only the provided keys and removing all others
func Filter(w *bytes.Buffer, b []byte, keys []string) error {
var err error
kmap := make(map[uint64]struct{}, len(keys))
h := maphash.Hash{}
for i := range keys {
_, _ = h.WriteString(keys[i])
kmap[h.Sum64()] = struct{}{}
h.Reset()
kmap[xxhash.Sum64String(keys[i])] = struct{}{}
}
// is an list
@ -134,11 +132,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
cb := b[s:(e + 1)]
e = 0
_, _ = h.Write(k)
_, ok := kmap[h.Sum64()]
h.Reset()
if !ok {
if _, ok := kmap[xxhash.Sum64(k)]; !ok {
continue
}

View File

@ -1,7 +1,7 @@
package jsn
import (
"hash/maphash"
"github.com/cespare/xxhash/v2"
)
const (
@ -41,12 +41,9 @@ func Value(b []byte) []byte {
// Keys function fetches values for the provided keys
func Get(b []byte, keys [][]byte) []Field {
kmap := make(map[uint64]struct{}, len(keys))
h := maphash.Hash{}
for i := range keys {
_, _ = h.Write(keys[i])
kmap[h.Sum64()] = struct{}{}
h.Reset()
kmap[xxhash.Sum64(keys[i])] = struct{}{}
}
res := make([]Field, 0, 20)
@ -144,9 +141,7 @@ func Get(b []byte, keys [][]byte) []Field {
}
if e != 0 {
_, _ = h.Write(k)
_, ok := kmap[h.Sum64()]
h.Reset()
_, ok := kmap[xxhash.Sum64(k)]
if ok {
res = append(res, Field{k, b[s:(e + 1)]})

View File

@ -3,7 +3,8 @@ package jsn
import (
"bytes"
"errors"
"hash/maphash"
"github.com/cespare/xxhash/v2"
)
// Replace function replaces key-value pairs provided in the `from` argument with those in the `to` argument
@ -17,7 +18,7 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
return err
}
h := maphash.Hash{}
h := xxhash.New()
tmap := make(map[uint64]int, len(from))
for i, f := range from {