From 2241364d00bb1a2782c0e265df2fbafe4ee22090 Mon Sep 17 00:00:00 2001 From: Vikram Rangnekar Date: Tue, 26 May 2020 19:41:28 -0400 Subject: [PATCH] fix: rewrite the sql args and variables codebase to use expression values --- README.md | 6 +- core/api.go | 4 +- core/args.go | 169 +++--------------- core/args_test.go | 13 -- core/build.go | 16 +- core/consts.go | 5 - core/core.go | 31 ++-- core/init.go | 30 ---- core/internal/psql/columns.go | 19 +-- core/internal/psql/fuzz.go | 5 +- core/internal/psql/insert.go | 13 +- core/internal/psql/mutate.go | 91 +++++----- core/internal/psql/mutate_test.go | 4 +- core/internal/psql/psql_test.go | 4 +- core/internal/psql/query.go | 235 ++++++++++++++++---------- core/internal/psql/query_test.go | 19 ++- core/internal/psql/tests.sql | 67 ++++---- core/internal/psql/update.go | 22 +-- core/internal/psql/update_test.go | 2 +- core/internal/psql/utils.go | 13 -- core/internal/qcode/config.go | 13 +- core/internal/qcode/qcode.go | 5 +- core/prepare.go | 48 +----- core/remote.go | 2 +- docs/website/docs/home.md | 4 +- docs/website/docs/intro.md | 2 + go.mod | 1 - go.sum | 6 +- internal/serv/cmd_migrate.go | 6 +- internal/serv/cmd_new.go | 21 +-- internal/serv/rice-box.go | 16 +- internal/serv/tmpl/cloudbuild.yaml | 10 +- internal/serv/tmpl/dev.yml | 6 +- internal/serv/tmpl/docker-compose.yml | 40 +---- internal/serv/tmpl/prod.yml | 4 +- 35 files changed, 397 insertions(+), 555 deletions(-) delete mode 100644 core/args_test.go delete mode 100644 core/internal/psql/utils.go diff --git a/README.md b/README.md index 1154d58..1d43bcc 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,9 @@ func main() { } }` - res, err := sg.GraphQL(context.Background(), query, nil) + ctx = context.WithValue(ctx, core.UserIDKey, 1) + + res, err := sg.GraphQL(ctx, query, nil) if err != nil { log.Fatal(err) } @@ -90,7 +92,7 @@ This compiler is what sits at the heart of Super Graph, with layers of useful fu - Fuzz tested for security - Database migrations tool - Database seeding tool -- Works with Postgres and YugabyteDB +- Works with Postgres and Yugabyte DB - OpenCensus Support: Zipkin, Prometheus, X-Ray, Stackdriver ## Documentation diff --git a/core/api.go b/core/api.go index 453ba60..e4740d3 100644 --- a/core/api.go +++ b/core/api.go @@ -32,7 +32,9 @@ } }` - res, err := sg.GraphQL(context.Background(), query, nil) + ctx = context.WithValue(ctx, core.UserIDKey, 1) + + res, err := sg.GraphQL(ctx, query, nil) if err != nil { log.Fatal(err) } diff --git a/core/args.go b/core/args.go index ef32244..9a309f6 100644 --- a/core/args.go +++ b/core/args.go @@ -1,76 +1,18 @@ package core import ( - "bytes" "encoding/json" "fmt" - "io" + "github.com/dosco/super-graph/core/internal/psql" "github.com/dosco/super-graph/jsn" ) -// argMap function is used to string replace variables with values by -// the fasttemplate code -func (c *scontext) argMap() func(w io.Writer, tag string) (int, error) { - return func(w io.Writer, tag string) (int, error) { - switch tag { - case "user_id_provider": - if v := c.Value(UserIDProviderKey); v != nil { - return io.WriteString(w, v.(string)) - } - return 0, argErr("user_id_provider") - - case "user_id": - if v := c.Value(UserIDKey); v != nil { - return io.WriteString(w, v.(string)) - } - return 0, argErr("user_id") - - case "user_role": - if v := c.Value(UserRoleKey); v != nil { - return io.WriteString(w, v.(string)) - } - return 0, argErr("user_role") - } - - fields := jsn.Get(c.vars, [][]byte{[]byte(tag)}) - - if len(fields) == 0 { - return 0, argErr(tag) - - } - v := fields[0].Value - - if isJsonScalarArray(v) { - return w.Write(jsonListToValues(v)) - } - - // Open and close quotes - if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { - fields[0].Value = v[1 : len(v)-1] - } - - if tag == "cursor" { - if bytes.EqualFold(v, []byte("null")) { - return io.WriteString(w, ``) - } - v1, err := c.sg.decrypt(string(fields[0].Value)) - if err != nil { - return 0, err - } - - return w.Write(v1) - } - - return w.Write(escSQuote(fields[0].Value)) - } -} - // argList function is used to create a list of arguments to pass -// to a prepared statement. FYI no escaping of single quotes is -// needed here -func (c *scontext) argList(args [][]byte) ([]interface{}, error) { - vars := make([]interface{}, len(args)) +// to a prepared statement. + +func (c *scontext) argList(md psql.Metadata) ([]interface{}, error) { + vars := make([]interface{}, len(md.Params)) var fields map[string]json.RawMessage var err error @@ -83,31 +25,30 @@ func (c *scontext) argList(args [][]byte) ([]interface{}, error) { } } - for i := range args { - av := args[i] - switch { - case bytes.Equal(av, []byte("user_id")): + for i, p := range md.Params { + switch p.Name { + case "user_id": if v := c.Value(UserIDKey); v != nil { vars[i] = v.(string) } else { - return nil, argErr("user_id") + return nil, argErr(p) } - case bytes.Equal(av, []byte("user_id_provider")): + case "user_id_provider": if v := c.Value(UserIDProviderKey); v != nil { vars[i] = v.(string) } else { - return nil, argErr("user_id_provider") + return nil, argErr(p) } - case bytes.Equal(av, []byte("user_role")): + case "user_role": if v := c.Value(UserRoleKey); v != nil { vars[i] = v.(string) } else { - return nil, argErr("user_role") + return nil, argErr(p) } - case bytes.Equal(av, []byte("cursor")): + case "cursor": if v, ok := fields["cursor"]; ok && v[0] == '"' { v1, err := c.sg.decrypt(string(v[1 : len(v)-1])) if err != nil { @@ -115,18 +56,22 @@ func (c *scontext) argList(args [][]byte) ([]interface{}, error) { } vars[i] = v1 } else { - return nil, argErr("cursor") + return nil, argErr(p) } default: - if v, ok := fields[string(av)]; ok { + if v, ok := fields[p.Name]; ok { + switch { + case p.IsArray && v[0] != '[': + return nil, fmt.Errorf("variable '%s' should be an array of type '%s'", p.Name, p.Type) + + case p.Type == "json" && v[0] != '[' && v[0] != '{': + return nil, fmt.Errorf("variable '%s' should be an array or object", p.Name) + } + switch v[0] { case '[', '{': - if isJsonScalarArray(v) { - vars[i] = jsonListToValues(v) - } else { - vars[i] = v - } + vars[i] = v default: var val interface{} @@ -137,7 +82,7 @@ func (c *scontext) argList(args [][]byte) ([]interface{}, error) { } } else { - return nil, argErr(string(av)) + return nil, argErr(p) } } } @@ -145,64 +90,6 @@ func (c *scontext) argList(args [][]byte) ([]interface{}, error) { return vars, nil } -// -func escSQuote(b []byte) []byte { - var buf *bytes.Buffer - s := 0 - for i := range b { - if b[i] == '\'' { - if buf == nil { - buf = &bytes.Buffer{} - } - buf.Write(b[s:i]) - buf.WriteString(`''`) - s = i + 1 - } - } - - if buf == nil { - return b - } - - l := len(b) - if s < (l - 1) { - buf.Write(b[s:l]) - } - return buf.Bytes() -} - -func isJsonScalarArray(b []byte) bool { - if b[0] != '[' || b[len(b)-1] != ']' { - return false - } - for i := range b { - switch b[i] { - case '{': - return false - case '[', ' ', '\t', '\n': - continue - default: - return true - } - } - return true -} - -func jsonListToValues(b []byte) []byte { - s := 0 - for i := 1; i < len(b)-1; i++ { - if b[i] == '"' && s%2 == 0 { - b[i] = '\'' - } - if b[i] == '\\' { - s++ - } else { - s = 0 - } - } - return b[1 : len(b)-1] -} - -func argErr(name string) error { - return fmt.Errorf("query requires variable '%s' to be set", name) +func argErr(p psql.Param) error { + return fmt.Errorf("required variable '%s' of type '%s' must be set", p.Name, p.Type) } diff --git a/core/args_test.go b/core/args_test.go deleted file mode 100644 index 2227feb..0000000 --- a/core/args_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package core - -import "testing" - -func TestEscQuote(t *testing.T) { - val := "That's the worst, don''t be calling me's again" - exp := "That''s the worst, don''''t be calling me''s again" - ret := escSQuote([]byte(val)) - - if exp != string(ret) { - t.Errorf("escSQuote failed: %s", string(ret)) - } -} diff --git a/core/build.go b/core/build.go index 4f77673..9089235 100644 --- a/core/build.go +++ b/core/build.go @@ -12,10 +12,10 @@ import ( ) type stmt struct { - role *Role - qc *qcode.QCode - skipped uint32 - sql string + role *Role + qc *qcode.QCode + md psql.Metadata + sql string } func (sg *SuperGraph) buildStmt(qt qcode.QType, query, vars []byte, role string) ([]stmt, error) { @@ -62,12 +62,11 @@ func (sg *SuperGraph) buildRoleStmt(query, vars []byte, role string) ([]stmt, er stmts := []stmt{stmt{role: ro, qc: qc}} w := &bytes.Buffer{} - skipped, err := sg.pc.Compile(qc, w, psql.Variables(vm)) + stmts[0].md, err = sg.pc.Compile(w, qc, psql.Variables(vm)) if err != nil { return nil, err } - stmts[0].skipped = skipped stmts[0].sql = w.String() return stmts, nil @@ -104,14 +103,13 @@ func (sg *SuperGraph) buildMultiStmt(query, vars []byte) ([]stmt, error) { } stmts = append(stmts, stmt{role: role, qc: qc}) + s := &stmts[len(stmts)-1] - skipped, err := sg.pc.Compile(qc, w, psql.Variables(vm)) + s.md, err = sg.pc.Compile(w, qc, psql.Variables(vm)) if err != nil { return nil, err } - s := &stmts[len(stmts)-1] - s.skipped = skipped s.sql = w.String() w.Reset() } diff --git a/core/consts.go b/core/consts.go index 6c4f6a1..3fe156c 100644 --- a/core/consts.go +++ b/core/consts.go @@ -5,11 +5,6 @@ import ( "errors" ) -const ( - openVar = "{{" - closeVar = "}}" -) - var ( errNotFound = errors.New("not found in prepared statements") ) diff --git a/core/core.go b/core/core.go index 883f4cc..bad7207 100644 --- a/core/core.go +++ b/core/core.go @@ -1,7 +1,6 @@ package core import ( - "bytes" "context" "database/sql" "encoding/json" @@ -10,8 +9,6 @@ import ( "github.com/dosco/super-graph/core/internal/psql" "github.com/dosco/super-graph/core/internal/qcode" - - "github.com/valyala/fasttemplate" ) type OpType int @@ -126,7 +123,7 @@ func (c *scontext) execQuery() ([]byte, error) { return nil, err } - if len(data) == 0 || st.skipped == 0 { + if len(data) == 0 || st.md.Skipped == 0 { return data, nil } @@ -181,7 +178,7 @@ func (c *scontext) resolvePreparedSQL() ([]byte, *stmt, error) { var root []byte var row *sql.Row - varsList, err := c.argList(ps.args) + varsList, err := c.argList(ps.st.md) if err != nil { return nil, nil, err } @@ -252,15 +249,23 @@ func (c *scontext) resolveSQL() ([]byte, *stmt, error) { return nil, nil, err } st := &stmts[0] + c.res.sql = st.sql - t := fasttemplate.New(st.sql, openVar, closeVar) - buf := &bytes.Buffer{} - - _, err = t.ExecuteFunc(buf, c.argMap()) + varList, err := c.argList(st.md) if err != nil { return nil, nil, err } - finalSQL := buf.String() + // finalSQL := buf.String() + + //// + + // _, err = t.ExecuteFunc(buf, c.argMap(st.md)) + // if err != nil { + // return nil, nil, err + // } + // finalSQL := buf.String() + + ///// // var stime time.Time @@ -275,9 +280,9 @@ func (c *scontext) resolveSQL() ([]byte, *stmt, error) { // defaultRole := c.role if useTx { - row = tx.QueryRowContext(c, finalSQL) + row = tx.QueryRowContext(c, st.sql, varList...) } else { - row = c.sg.db.QueryRowContext(c, finalSQL) + row = c.sg.db.QueryRowContext(c, st.sql, varList...) } if len(stmts) > 1 { @@ -286,8 +291,6 @@ func (c *scontext) resolveSQL() ([]byte, *stmt, error) { err = row.Scan(&root) } - c.res.sql = finalSQL - if len(role) == 0 { c.res.role = c.role } else { diff --git a/core/init.go b/core/init.go index 187750c..3a3a418 100644 --- a/core/init.go +++ b/core/init.go @@ -2,9 +2,7 @@ package core import ( "fmt" - "regexp" "strings" - "unicode" "github.com/dosco/super-graph/core/internal/psql" "github.com/dosco/super-graph/core/internal/qcode" @@ -18,11 +16,6 @@ func (sg *SuperGraph) initConfig() error { flect.AddPlural(k, v) } - // Variables: Validate and sanitize - for k, v := range c.Vars { - c.Vars[k] = sanitizeVars(v) - } - // Tables: Validate and sanitize tm := make(map[string]struct{}) @@ -80,9 +73,6 @@ func (sg *SuperGraph) initConfig() error { sg.roles["anon"] = &ur } - // Roles: validate and sanitize - c.RolesQuery = sanitizeVars(c.RolesQuery) - if c.RolesQuery == "" { sg.log.Printf("WRN roles_query not defined: attribute based access control disabled") } @@ -293,23 +283,3 @@ func (r *Role) GetTable(name string) *RoleTable { func sanitize(value string) string { return strings.ToLower(strings.TrimSpace(value)) } - -var ( - varRe1 = regexp.MustCompile(`(?mi)\$([a-zA-Z0-9_.]+)`) - varRe2 = regexp.MustCompile(`\{\{([a-zA-Z0-9_.]+)\}\}`) -) - -func sanitizeVars(s string) string { - s0 := varRe1.ReplaceAllString(s, `{{$1}}`) - - s1 := strings.Map(func(r rune) rune { - if unicode.IsSpace(r) { - return ' ' - } - return r - }, s0) - - return varRe2.ReplaceAllStringFunc(s1, func(m string) string { - return strings.ToLower(m) - }) -} diff --git a/core/internal/psql/columns.go b/core/internal/psql/columns.go index a9f6b83..d339dc9 100644 --- a/core/internal/psql/columns.go +++ b/core/internal/psql/columns.go @@ -12,8 +12,7 @@ import ( func (c *compilerContext) renderBaseColumns( sel *qcode.Select, ti *DBTableInfo, - childCols []*qcode.Column, - skipped uint32) ([]int, bool, error) { + childCols []*qcode.Column) ([]int, bool, error) { var realColsRendered []int @@ -116,12 +115,12 @@ func (c *compilerContext) renderColumnSearchRank(sel *qcode.Select, ti *DBTableI 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(`) } - io.WriteString(c.w, arg.Val) - io.WriteString(c.w, `}}'))`) + c.renderValueExp(Param{Name: arg.Val, Type: "string"}) + io.WriteString(c.w, `))`) alias(c.w, col.Name) return nil @@ -141,12 +140,12 @@ func (c *compilerContext) renderColumnSearchHeadline(sel *qcode.Select, ti *DBTa 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(`) } - io.WriteString(c.w, arg.Val) - io.WriteString(c.w, `}}'))`) + c.renderValueExp(Param{Name: arg.Val, Type: "string"}) + io.WriteString(c.w, `))`) alias(c.w, col.Name) return nil diff --git a/core/internal/psql/fuzz.go b/core/internal/psql/fuzz.go index 12d63c5..bcdf4ef 100644 --- a/core/internal/psql/fuzz.go +++ b/core/internal/psql/fuzz.go @@ -4,6 +4,7 @@ package psql import ( "encoding/json" + "github.com/dosco/super-graph/core/internal/qcode" ) @@ -12,9 +13,9 @@ var ( schema = GetTestSchema() - vars = NewVariables(map[string]string{ + vars = map[string]string{ "admin_account_id": "5", - }) + } pcompileTest = NewCompiler(Config{ Schema: schema, diff --git a/core/internal/psql/insert.go b/core/internal/psql/insert.go index b4d8c87..21dc243 100644 --- a/core/internal/psql/insert.go +++ b/core/internal/psql/insert.go @@ -10,8 +10,8 @@ import ( "github.com/dosco/super-graph/core/internal/util" ) -func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, - vars Variables, ti *DBTableInfo) (uint32, error) { +func (c *compilerContext) renderInsert( + w io.Writer, qc *qcode.QCode, vars Variables, ti *DBTableInfo) (uint32, error) { insert, ok := vars[qc.ActionVar] if !ok { @@ -25,9 +25,8 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, if insert[0] == '[' { io.WriteString(c.w, `json_array_elements(`) } - io.WriteString(c.w, `'{{`) - io.WriteString(c.w, qc.ActionVar) - io.WriteString(c.w, `}}' :: json`) + c.renderValueExp(Param{Name: qc.ActionVar, Type: "json"}) + io.WriteString(c.w, ` :: json`) if insert[0] == '[' { io.WriteString(c.w, `)`) } @@ -90,12 +89,12 @@ func (c *compilerContext) renderInsertStmt(qc *qcode.QCode, w io.Writer, item re io.WriteString(w, `INSERT INTO `) quoted(w, ti.Name) io.WriteString(w, ` (`) - renderInsertUpdateColumns(w, qc, jt, ti, sk, false) + c.renderInsertUpdateColumns(qc, jt, ti, sk, false) renderNestedInsertRelColumns(w, item.kvitem, false) io.WriteString(w, `)`) io.WriteString(w, ` SELECT `) - renderInsertUpdateColumns(w, qc, jt, ti, sk, true) + c.renderInsertUpdateColumns(qc, jt, ti, sk, true) renderNestedInsertRelColumns(w, item.kvitem, true) io.WriteString(w, ` FROM "_sg_input" i`) diff --git a/core/internal/psql/mutate.go b/core/internal/psql/mutate.go index 66dfb22..79ef6ae 100644 --- a/core/internal/psql/mutate.go +++ b/core/internal/psql/mutate.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "strings" "github.com/dosco/super-graph/core/internal/qcode" "github.com/dosco/super-graph/core/internal/util" @@ -33,42 +34,44 @@ var updateTypes = map[string]itemType{ var noLimit = qcode.Paging{NoLimit: true} -func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) { +func (co *Compiler) compileMutation(w io.Writer, qc *qcode.QCode, vars Variables) (Metadata, error) { + md := Metadata{} + if len(qc.Selects) == 0 { - return 0, errors.New("empty query") + return md, errors.New("empty query") } - c := &compilerContext{w, qc.Selects, co} + c := &compilerContext{md, w, qc.Selects, co} root := &qc.Selects[0] ti, err := c.schema.GetTable(root.Name) if err != nil { - return 0, err + return c.md, err } switch qc.Type { case qcode.QTInsert: - if _, err := c.renderInsert(qc, w, vars, ti); err != nil { - return 0, err + if _, err := c.renderInsert(w, qc, vars, ti); err != nil { + return c.md, err } case qcode.QTUpdate: - if _, err := c.renderUpdate(qc, w, vars, ti); err != nil { - return 0, err + if _, err := c.renderUpdate(w, qc, vars, ti); err != nil { + return c.md, err } case qcode.QTUpsert: - if _, err := c.renderUpsert(qc, w, vars, ti); err != nil { - return 0, err + if _, err := c.renderUpsert(w, qc, vars, ti); err != nil { + return c.md, err } case qcode.QTDelete: - if _, err := c.renderDelete(qc, w, vars, ti); err != nil { - return 0, err + if _, err := c.renderDelete(w, qc, vars, ti); err != nil { + return c.md, err } default: - return 0, errors.New("valid mutations are 'insert', 'update', 'upsert' and 'delete'") + return c.md, errors.New("valid mutations are 'insert', 'update', 'upsert' and 'delete'") } root.Paging = noLimit @@ -77,7 +80,7 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables root.Where = nil root.Args = nil - return c.compileQuery(qc, w, vars) + return co.compileQueryWithMetadata(w, qc, vars, c.md) } type kvitem struct { @@ -365,12 +368,12 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error { return nil } -func renderInsertUpdateColumns(w io.Writer, +func (c *compilerContext) renderInsertUpdateColumns( qc *qcode.QCode, jt map[string]json.RawMessage, ti *DBTableInfo, skipcols map[string]struct{}, - values bool) (uint32, error) { + isValues bool) (uint32, error) { root := &qc.Selects[0] renderedCol := false @@ -392,18 +395,18 @@ func renderInsertUpdateColumns(w io.Writer, } } if n != 0 { - io.WriteString(w, `, `) + io.WriteString(c.w, `, `) } - if values { - io.WriteString(w, `CAST( i.j ->>`) - io.WriteString(w, `'`) - io.WriteString(w, cn.Name) - io.WriteString(w, `' AS `) - io.WriteString(w, cn.Type) - io.WriteString(w, `)`) + if isValues { + io.WriteString(c.w, `CAST( i.j ->>`) + io.WriteString(c.w, `'`) + io.WriteString(c.w, cn.Name) + io.WriteString(c.w, `' AS `) + io.WriteString(c.w, cn.Type) + io.WriteString(c.w, `)`) } else { - quoted(w, cn.Name) + quoted(c.w, cn.Name) } if !renderedCol { @@ -422,16 +425,28 @@ func renderInsertUpdateColumns(w io.Writer, continue } if i != 0 || n != 0 { - io.WriteString(w, `, `) + io.WriteString(c.w, `, `) } - if values { - io.WriteString(w, `'`) - io.WriteString(w, root.PresetMap[cn]) - io.WriteString(w, `' :: `) - io.WriteString(w, col.Type) + if isValues { + val := root.PresetMap[cn] + switch { + case ok && len(val) > 1 && val[0] == '$': + c.renderValueExp(Param{Name: val[1:], Type: col.Type}) + + case ok && strings.HasPrefix(val, "sql:"): + io.WriteString(c.w, `(`) + c.renderVar(val[4:], c.renderValueExp) + io.WriteString(c.w, `)`) + + case ok: + squoted(c.w, val) + } + + io.WriteString(c.w, ` :: `) + io.WriteString(c.w, col.Type) } else { - quoted(w, cn) + quoted(c.w, cn) } if !renderedCol { @@ -440,15 +455,15 @@ func renderInsertUpdateColumns(w io.Writer, } if len(skipcols) != 0 && renderedCol { - io.WriteString(w, `, `) + io.WriteString(c.w, `, `) } return 0, nil } -func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer, - vars Variables, ti *DBTableInfo) (uint32, error) { - root := &qc.Selects[0] +func (c *compilerContext) renderUpsert( + w io.Writer, qc *qcode.QCode, vars Variables, ti *DBTableInfo) (uint32, error) { + root := &qc.Selects[0] upsert, ok := vars[qc.ActionVar] if !ok { return 0, fmt.Errorf("variable '%s' not defined", qc.ActionVar) @@ -466,7 +481,7 @@ func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer, return 0, err } - if _, err := c.renderInsert(qc, w, vars, ti); err != nil { + if _, err := c.renderInsert(w, qc, vars, ti); err != nil { return 0, err } @@ -672,7 +687,7 @@ func renderCteName(w io.Writer, item kvitem) error { io.WriteString(w, item.ti.Name) if item._type == itemConnect || item._type == itemDisconnect { io.WriteString(w, `_`) - int2string(w, item.id) + int32String(w, item.id) } io.WriteString(w, `"`) return nil diff --git a/core/internal/psql/mutate_test.go b/core/internal/psql/mutate_test.go index ffd99d5..773b424 100644 --- a/core/internal/psql/mutate_test.go +++ b/core/internal/psql/mutate_test.go @@ -72,7 +72,7 @@ func delete(t *testing.T) { // } // }` -// sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` +// sql := `WITH "users" AS (WITH "input" AS (SELECT '$1' :: json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` // vars := map[string]json.RawMessage{ // "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), @@ -97,7 +97,7 @@ func delete(t *testing.T) { // } // }` -// sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` +// sql := `WITH "users" AS (WITH "input" AS (SELECT '$1' :: json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` // vars := map[string]json.RawMessage{ // "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), diff --git a/core/internal/psql/psql_test.go b/core/internal/psql/psql_test.go index bad3f1f..867de70 100644 --- a/core/internal/psql/psql_test.go +++ b/core/internal/psql/psql_test.go @@ -139,9 +139,9 @@ func TestMain(m *testing.M) { log.Fatal(err) } - vars := psql.NewVariables(map[string]string{ + vars := map[string]string{ "admin_account_id": "5", - }) + } pcompile = psql.NewCompiler(psql.Config{ Schema: schema, diff --git a/core/internal/psql/query.go b/core/internal/psql/query.go index b7acf48..58bfc92 100644 --- a/core/internal/psql/query.go +++ b/core/internal/psql/query.go @@ -17,6 +17,25 @@ const ( closeBlock = 500 ) +type Param struct { + Name string + Type string + IsArray bool +} + +type Metadata struct { + Skipped uint32 + Params []Param + pindex map[string]int +} + +type compilerContext struct { + md Metadata + w io.Writer + s []qcode.Select + *Compiler +} + type Variables map[string]json.RawMessage type Config struct { @@ -36,12 +55,12 @@ func NewCompiler(conf Config) *Compiler { } } -func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error { - return c.schema.SetRel(child, parent, rel) +func (co *Compiler) AddRelationship(child, parent string, rel *DBRel) error { + return co.schema.SetRel(child, parent, rel) } -func (c *Compiler) IDColumn(table string) (*DBColumn, error) { - ti, err := c.schema.GetTable(table) +func (co *Compiler) IDColumn(table string) (*DBColumn, error) { + ti, err := co.schema.GetTable(table) if err != nil { return nil, err } @@ -53,36 +72,39 @@ func (c *Compiler) IDColumn(table string) (*DBColumn, error) { return ti.PrimaryCol, nil } -type compilerContext struct { - w io.Writer - s []qcode.Select - *Compiler -} - -func (co *Compiler) CompileEx(qc *qcode.QCode, vars Variables) (uint32, []byte, error) { +func (co *Compiler) CompileEx(qc *qcode.QCode, vars Variables) (Metadata, []byte, error) { w := &bytes.Buffer{} - skipped, err := co.Compile(qc, w, vars) - return skipped, w.Bytes(), err + metad, err := co.Compile(w, qc, vars) + return metad, w.Bytes(), err } -func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) { +func (co *Compiler) Compile(w io.Writer, qc *qcode.QCode, vars Variables) (Metadata, error) { switch qc.Type { case qcode.QTQuery: - return co.compileQuery(qc, w, vars) - case qcode.QTInsert, qcode.QTUpdate, qcode.QTDelete, qcode.QTUpsert: - return co.compileMutation(qc, w, vars) + return co.compileQuery(w, qc, vars) + + case qcode.QTInsert, + qcode.QTUpdate, + qcode.QTDelete, + qcode.QTUpsert: + return co.compileMutation(w, qc, vars) } - return 0, fmt.Errorf("Unknown operation type %d", qc.Type) + return Metadata{}, fmt.Errorf("Unknown operation type %d", qc.Type) } -func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) { +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( + w io.Writer, qc *qcode.QCode, vars Variables, md Metadata) (Metadata, error) { + if len(qc.Selects) == 0 { - return 0, errors.New("empty query") + return md, errors.New("empty query") } - c := &compilerContext{w, qc.Selects, co} - + c := &compilerContext{md, w, qc.Selects, co} st := NewIntStack() i := 0 @@ -108,13 +130,11 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) ( i++ } - var ignored uint32 - if st.Len() != 0 { io.WriteString(c.w, `) as "__root" FROM `) } else { io.WriteString(c.w, `) as "__root"`) - return ignored, nil + return c.md, nil } for { @@ -133,7 +153,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) ( ti, err := c.schema.GetTable(sel.Name) if err != nil { - return 0, err + return c.md, err } if sel.ParentID == -1 { @@ -146,14 +166,12 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) ( c.renderPluralSelect(sel, ti) } - skipped, err := c.renderSelect(sel, ti, vars) - if err != nil { - return 0, err + if err := c.renderSelect(sel, ti, vars); err != nil { + return c.md, err } - ignored |= skipped for _, cid := range sel.Children { - if hasBit(skipped, uint32(cid)) { + if hasBit(c.md.Skipped, uint32(cid)) { continue } child := &c.s[cid] @@ -170,7 +188,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) ( ti, err := c.schema.GetTable(sel.Name) if err != nil { - return 0, err + return c.md, err } io.WriteString(c.w, `)`) @@ -202,12 +220,12 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) ( } } - return ignored, nil + return c.md, nil } func (c *compilerContext) renderPluralSelect(sel *qcode.Select, ti *DBTableInfo) error { io.WriteString(c.w, `SELECT coalesce(jsonb_agg("__sj_`) - int2string(c.w, sel.ID) + int32String(c.w, sel.ID) io.WriteString(c.w, `"."json"), '[]') as "json"`) if sel.Paging.Type != qcode.PtOffset { @@ -231,7 +249,7 @@ func (c *compilerContext) renderPluralSelect(sel *qcode.Select, ti *DBTableInfo) io.WriteString(c.w, `, CONCAT_WS(','`) for i := 0; i < n; i++ { io.WriteString(c.w, `, max("__cur_`) - int2string(c.w, int32(i)) + int32String(c.w, int32(i)) io.WriteString(c.w, `")`) } io.WriteString(c.w, `) as "cursor"`) @@ -247,7 +265,7 @@ func (c *compilerContext) renderRootSelect(sel *qcode.Select) error { io.WriteString(c.w, `', `) io.WriteString(c.w, `"__sj_`) - int2string(c.w, sel.ID) + int32String(c.w, sel.ID) io.WriteString(c.w, `"."json"`) if sel.Paging.Type != qcode.PtOffset { @@ -256,16 +274,14 @@ func (c *compilerContext) renderRootSelect(sel *qcode.Select) error { io.WriteString(c.w, `_cursor', `) io.WriteString(c.w, `"__sj_`) - int2string(c.w, sel.ID) + int32String(c.w, sel.ID) io.WriteString(c.w, `"."cursor"`) } return nil } -func (c *compilerContext) initSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) (uint32, []*qcode.Column, error) { - var skipped uint32 - +func (c *compilerContext) initSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) ([]*qcode.Column, error) { cols := make([]*qcode.Column, 0, len(sel.Cols)) colmap := make(map[string]struct{}, len(sel.Cols)) @@ -307,9 +323,7 @@ func (c *compilerContext) initSelect(sel *qcode.Select, ti *DBTableInfo, vars Va rel, err := c.schema.GetRel(child.Name, ti.Name) if err != nil { - return 0, nil, err - //skipped |= (1 << uint(id)) - //continue + return nil, err } switch rel.Type { @@ -335,16 +349,15 @@ 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{}{} - skipped |= (1 << uint(id)) + c.md.Skipped |= (1 << uint(id)) } default: - return 0, nil, fmt.Errorf("unknown relationship %s", rel) - //skipped |= (1 << uint(id)) + return nil, fmt.Errorf("unknown relationship %s", rel) } } - return skipped, cols, nil + return cols, nil } // This @@ -413,7 +426,7 @@ func (c *compilerContext) addSeekPredicate(sel *qcode.Select) error { return nil } -func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) (uint32, error) { +func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) error { var rel *DBRel var err error @@ -422,13 +435,13 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars rel, err = c.schema.GetRel(ti.Name, parent.Name) if err != nil { - return 0, err + return err } } - skipped, childCols, err := c.initSelect(sel, ti, vars) + childCols, err := c.initSelect(sel, ti, vars) if err != nil { - return 0, err + return err } // SELECT @@ -438,13 +451,13 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars // } io.WriteString(c.w, `SELECT to_jsonb("__sr_`) - int2string(c.w, sel.ID) + int32String(c.w, sel.ID) io.WriteString(c.w, `".*) `) if sel.Paging.Type != qcode.PtOffset { for i := range sel.OrderBy { io.WriteString(c.w, `- '__cur_`) - int2string(c.w, int32(i)) + int32String(c.w, int32(i)) io.WriteString(c.w, `' `) } } @@ -454,15 +467,15 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars if sel.Paging.Type != qcode.PtOffset { for i := range sel.OrderBy { io.WriteString(c.w, `, "__cur_`) - int2string(c.w, int32(i)) + int32String(c.w, int32(i)) io.WriteString(c.w, `"`) } } io.WriteString(c.w, `FROM (SELECT `) - if err := c.renderColumns(sel, ti, skipped); err != nil { - return 0, err + if err := c.renderColumns(sel, ti); err != nil { + return err } if sel.Paging.Type != qcode.PtOffset { @@ -470,7 +483,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars io.WriteString(c.w, `, LAST_VALUE(`) colWithTableID(c.w, ti.Name, sel.ID, ob.Col) io.WriteString(c.w, `) OVER() AS "__cur_`) - int2string(c.w, int32(i)) + int32String(c.w, int32(i)) io.WriteString(c.w, `"`) } } @@ -478,9 +491,8 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars io.WriteString(c.w, ` FROM (`) // FROM (SELECT .... ) - err = c.renderBaseSelect(sel, ti, rel, childCols, skipped) - if err != nil { - return skipped, err + if err = c.renderBaseSelect(sel, ti, rel, childCols); err != nil { + return err } //fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID) @@ -489,7 +501,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars // END-FROM - return skipped, nil + return nil } func (c *compilerContext) renderLateralJoin(sel *qcode.Select) error { @@ -539,7 +551,7 @@ func (c *compilerContext) renderJoinByName(table, parent string, id int32) error return nil } -func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error { +func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) error { i := 0 var cn string @@ -575,7 +587,7 @@ func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo, skip i += c.renderRemoteRelColumns(sel, ti, i) - return c.renderJoinColumns(sel, ti, skipped, i) + return c.renderJoinColumns(sel, ti, i) } func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo, colsRendered int) int { @@ -600,12 +612,12 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableI return i } -func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32, colsRendered int) error { +func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo, colsRendered int) error { // columns previously rendered i := colsRendered for _, id := range sel.Children { - if hasBit(skipped, uint32(id)) { + if hasBit(c.md.Skipped, uint32(id)) { continue } childSel := &c.s[id] @@ -621,13 +633,13 @@ func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo, } io.WriteString(c.w, `"__sj_`) - int2string(c.w, childSel.ID) + 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_`) - int2string(c.w, childSel.ID) + int32String(c.w, childSel.ID) io.WriteString(c.w, `"."cursor" AS "`) io.WriteString(c.w, childSel.FieldName) io.WriteString(c.w, `_cursor"`) @@ -640,7 +652,7 @@ func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo, } func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, rel *DBRel, - childCols []*qcode.Column, skipped uint32) error { + childCols []*qcode.Column) error { isRoot := (rel == nil) isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop) hasOrder := len(sel.OrderBy) != 0 @@ -655,7 +667,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r c.renderDistinctOn(sel, ti) } - realColsRendered, isAgg, err := c.renderBaseColumns(sel, ti, childCols, skipped) + realColsRendered, isAgg, err := c.renderBaseColumns(sel, ti, childCols) if err != nil { return err } @@ -782,11 +794,13 @@ func (c *compilerContext) renderCursorCTE(sel *qcode.Select) error { io.WriteString(c.w, `, `) } io.WriteString(c.w, `a[`) - int2string(c.w, int32(i+1)) + int32String(c.w, int32(i+1)) io.WriteString(c.w, `] as `) quoted(c.w, ob.Col) } - io.WriteString(c.w, ` FROM string_to_array('{{cursor}}', ',') as a) `) + io.WriteString(c.w, ` FROM string_to_array(`) + c.renderValueExp(Param{Name: "cursor", Type: "json"}) + io.WriteString(c.w, `, ',') as a) `) return nil } @@ -1079,12 +1093,13 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error { io.WriteString(c.w, `((`) colWithTable(c.w, ti.Name, ti.TSVCol.Name) 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(`) } - io.WriteString(c.w, ex.Val) - io.WriteString(c.w, `}}'))`) + c.renderValueExp(Param{Name: ex.Val, Type: "string"}) + io.WriteString(c.w, `))`) + return nil default: @@ -1170,15 +1185,17 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col * val, ok := vars[ex.Val] switch { case ok && strings.HasPrefix(val, "sql:"): - io.WriteString(c.w, ` (`) - io.WriteString(c.w, val[4:]) + io.WriteString(c.w, `(`) + c.renderVar(val[4:], c.renderValueExp) io.WriteString(c.w, `)`) + case ok: squoted(c.w, val) + case ex.Op == qcode.OpIn || ex.Op == qcode.OpNotIn: - io.WriteString(c.w, ` (string_to_array('{{`) - io.WriteString(c.w, ex.Val) - io.WriteString(c.w, `}}', ',')`) + io.WriteString(c.w, `(ARRAY(SELECT json_array_elements_text(`) + c.renderValueExp(Param{Name: ex.Val, Type: col.Type, IsArray: true}) + io.WriteString(c.w, `))`) io.WriteString(c.w, ` :: `) io.WriteString(c.w, col.Type) @@ -1186,9 +1203,7 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col * return default: - io.WriteString(c.w, ` '{{`) - io.WriteString(c.w, ex.Val) - io.WriteString(c.w, `}}'`) + c.renderValueExp(Param{Name: ex.Val, Type: col.Type, IsArray: false}) } case qcode.ValRef: @@ -1202,6 +1217,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_"): @@ -1253,7 +1316,7 @@ func aliasWithID(w io.Writer, alias string, id int32) { io.WriteString(w, ` AS "`) io.WriteString(w, alias) io.WriteString(w, `_`) - int2string(w, id) + int32String(w, id) io.WriteString(w, `"`) } @@ -1270,7 +1333,7 @@ func colWithTableID(w io.Writer, table string, id int32, col string) { io.WriteString(w, table) if id >= 0 { io.WriteString(w, `_`) - int2string(w, id) + int32String(w, id) } io.WriteString(w, `"."`) io.WriteString(w, col) @@ -1291,7 +1354,7 @@ func squoted(w io.Writer, identifier string) { const charset = "0123456789" -func int2string(w io.Writer, val int32) { +func int32String(w io.Writer, val int32) { if val < 10 { w.Write([]byte{charset[val]}) return diff --git a/core/internal/psql/query_test.go b/core/internal/psql/query_test.go index 5e88cf9..6960584 100644 --- a/core/internal/psql/query_test.go +++ b/core/internal/psql/query_test.go @@ -32,6 +32,20 @@ func withComplexArgs(t *testing.T) { compileGQLToPSQL(t, gql, nil, "user") } +func withWhereIn(t *testing.T) { + gql := `query { + products(where: { id: { in: $list } }) { + id + } + }` + + vars := map[string]json.RawMessage{ + "list": json.RawMessage(`[1,2,3]`), + } + + compileGQLToPSQL(t, gql, vars, "user") +} + func withWhereAndList(t *testing.T) { gql := `query { products( @@ -367,6 +381,7 @@ func blockedFunctions(t *testing.T) { func TestCompileQuery(t *testing.T) { t.Run("withComplexArgs", withComplexArgs) + t.Run("withWhereIn", withWhereIn) t.Run("withWhereAndList", withWhereAndList) t.Run("withWhereIsNull", withWhereIsNull) t.Run("withWhereMultiOr", withWhereMultiOr) @@ -429,7 +444,7 @@ func BenchmarkCompile(b *testing.B) { b.Fatal(err) } - _, err = pcompile.Compile(qc, w, nil) + _, err = pcompile.Compile(w, qc, nil) if err != nil { b.Fatal(err) } @@ -450,7 +465,7 @@ func BenchmarkCompileParallel(b *testing.B) { b.Fatal(err) } - _, err = pcompile.Compile(qc, w, nil) + _, err = pcompile.Compile(w, qc, nil) if err != nil { b.Fatal(err) } diff --git a/core/internal/psql/tests.sql b/core/internal/psql/tests.sql index fc6808b..c5feb4c 100644 --- a/core/internal/psql/tests.sql +++ b/core/internal/psql/tests.sql @@ -1,25 +1,25 @@ === RUN TestCompileInsert === RUN TestCompileInsert/simpleInsert -WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i 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" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0" +WITH "_sg_input" AS (SELECT $1 :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i 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" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileInsert/singleInsert -WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'user_id' AS 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", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'user_id' AS 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", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileInsert/bulkInsert -WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) 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", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) 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", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileInsert/simpleInsertWithPresets -WITH "_sg_input" AS (SELECT '{{data}}' :: 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, '{{user_id}}' :: 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" +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 '{{data}}' :: 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 '{{data}}' :: 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), "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 '{{data}}' :: 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" +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 -WITH "_sg_input" AS (SELECT '{{data}}' :: 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('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), "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('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 TestCompileInsert/nestedInsertOneToManyWithConnect -WITH "_sg_input" AS (SELECT '{{data}}' :: 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 ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*) 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" +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 ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*) 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/nestedInsertOneToOneWithConnect -WITH "_sg_input" AS (SELECT '{{data}}' :: 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" +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 '{{data}}' :: 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" +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.02s) --- PASS: TestCompileInsert/simpleInsert (0.00s) --- PASS: TestCompileInsert/singleInsert (0.00s) @@ -33,14 +33,14 @@ WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" --- PASS: TestCompileInsert/nestedInsertOneToOneWithConnectArray (0.00s) === RUN TestCompileMutate === RUN TestCompileMutate/singleUpsert -WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileMutate/singleUpsertWhere -WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) WHERE (("products"."price") > '3' :: numeric(7,2)) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) WHERE (("products"."price") > '3' :: numeric(7,2)) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileMutate/bulkUpsert -WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileMutate/delete WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '1' :: 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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" ---- PASS: TestCompileMutate (0.00s) +--- PASS: TestCompileMutate (0.01s) --- PASS: TestCompileMutate/singleUpsert (0.00s) --- PASS: TestCompileMutate/singleUpsertWhere (0.00s) --- PASS: TestCompileMutate/bulkUpsert (0.00s) @@ -48,6 +48,8 @@ WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > '0' : === RUN TestCompileQuery === RUN TestCompileQuery/withComplexArgs 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", "products_0"."price" AS "price" FROM (SELECT DISTINCT ON ("products"."price") "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."id") < '28' :: bigint) AND (("products"."id") >= '20' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) ORDER BY "products"."price" DESC LIMIT ('30') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" +=== RUN TestCompileQuery/withWhereIn +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" FROM (SELECT "products"."id" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = ANY (ARRAY(SELECT json_array_elements_text($1)) :: bigint[])))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" === RUN TestCompileQuery/withWhereAndList 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", "products_0"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" === RUN TestCompileQuery/withWhereIsNull @@ -55,9 +57,9 @@ SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT === RUN TestCompileQuery/withWhereMultiOr 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", "products_0"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") < '20' :: numeric(7,2)) OR (("products"."price") > '10' :: numeric(7,2)) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" === RUN TestCompileQuery/fetchByID -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" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '{{id}}' :: bigint))) LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +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" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = $1 :: bigint))) LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileQuery/searchQuery -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", "products_0"."search_rank" AS "search_rank", "products_0"."search_headline_description" AS "search_headline_description" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('{{query}}')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('{{query}}')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('{{query}}'))) LIMIT ('20') :: integer) AS "products_0") 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"."id" AS "id", "products_0"."name" AS "name", "products_0"."search_rank" AS "search_rank", "products_0"."search_headline_description" AS "search_headline_description" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery($1)) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery($1)) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery($1))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" === RUN TestCompileQuery/oneToMany 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"."email" AS "email", "__sj_1"."json" AS "products" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_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", "products_1"."price" AS "price" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."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/oneToManyReverse @@ -77,9 +79,9 @@ SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT === RUN TestCompileQuery/aggFunctionWithFilter 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"."max_price" AS "max_price" FROM (SELECT "products"."id", max("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") > '10' :: bigint))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" === RUN TestCompileQuery/syntheticTables -SELECT jsonb_build_object('me', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0" +SELECT jsonb_build_object('me', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = $1 :: bigint)) LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileQuery/queryWithVariables -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" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") = '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +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" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") = $1 :: numeric(7,2)) AND (("products"."id") = $2 :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === 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 @@ -87,15 +89,16 @@ SELECT jsonb_build_object('customer', "__sj_0"."json", 'user', "__sj_1"."json", === 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 -SELECT jsonb_build_object('products', "__sj_0"."json", 'products_cursor', "__sj_0"."cursor") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json", CONCAT_WS(',', max("__cur_0"), max("__cur_1")) as "cursor" FROM (SELECT to_jsonb("__sr_0".*) - '__cur_0' - '__cur_1' AS "json", "__cur_0", "__cur_1"FROM (SELECT "products_0"."name" AS "name", LAST_VALUE("products_0"."price") OVER() AS "__cur_0", LAST_VALUE("products_0"."id") OVER() AS "__cur_1" FROM (WITH "__cur" AS (SELECT a[1] as "price", a[2] as "id" FROM string_to_array('{{cursor}}', ',') as a) SELECT "products"."name", "products"."id", "products"."price" FROM "products", "__cur" WHERE (((("products"."price") < "__cur"."price" :: numeric(7,2)) OR ((("products"."price") = "__cur"."price" :: numeric(7,2)) AND (("products"."id") > "__cur"."id" :: bigint)))) ORDER BY "products"."price" DESC, "products"."id" ASC LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" +SELECT jsonb_build_object('products', "__sj_0"."json", 'products_cursor', "__sj_0"."cursor") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json", CONCAT_WS(',', max("__cur_0"), max("__cur_1")) as "cursor" FROM (SELECT to_jsonb("__sr_0".*) - '__cur_0' - '__cur_1' AS "json", "__cur_0", "__cur_1"FROM (SELECT "products_0"."name" AS "name", LAST_VALUE("products_0"."price") OVER() AS "__cur_0", LAST_VALUE("products_0"."id") OVER() AS "__cur_1" FROM (WITH "__cur" AS (SELECT a[1] as "price", a[2] as "id" FROM string_to_array($1, ',') as a) SELECT "products"."name", "products"."id", "products"."price" FROM "products", "__cur" WHERE (((("products"."price") < "__cur"."price" :: numeric(7,2)) OR ((("products"."price") = "__cur"."price" :: numeric(7,2)) AND (("products"."id") > "__cur"."id" :: bigint)))) ORDER BY "products"."price" DESC, "products"."id" ASC LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" === RUN TestCompileQuery/nullForAuthRequiredInAnon 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", NULL AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" === RUN TestCompileQuery/blockedQuery 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" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileQuery/blockedFunctions 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"."email" AS "email" FROM (SELECT , "users"."email" FROM "users" WHERE (false) GROUP BY "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0" ---- PASS: TestCompileQuery (0.02s) +--- PASS: TestCompileQuery (0.03s) --- PASS: TestCompileQuery/withComplexArgs (0.00s) + --- PASS: TestCompileQuery/withWhereIn (0.00s) --- PASS: TestCompileQuery/withWhereAndList (0.00s) --- PASS: TestCompileQuery/withWhereIsNull (0.00s) --- PASS: TestCompileQuery/withWhereMultiOr (0.00s) @@ -121,23 +124,23 @@ SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coa --- PASS: TestCompileQuery/blockedFunctions (0.00s) === RUN TestCompileUpdate === RUN TestCompileUpdate/singleUpdate -WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i) WHERE ((("products"."id") = '1' :: bigint) AND (("products"."id") = '{{id}}' :: 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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i) WHERE ((("products"."id") = '1' :: bigint) AND (("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" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileUpdate/simpleUpdateWithPresets -WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), 'now' :: timestamp without time zone FROM "_sg_input" i) WHERE (("products"."user_id") = '{{user_id}}' :: 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" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" +WITH "_sg_input" AS (SELECT $1 :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), 'now' :: timestamp without time zone FROM "_sg_input" i) WHERE (("products"."user_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" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0" === RUN TestCompileUpdate/nestedUpdateManyToMany -WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (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) FROM "_sg_input" i) WHERE (("purchases"."id") = '{{id}}' :: bigint) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) 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 '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (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) FROM "_sg_input" i) WHERE (("purchases"."id") = '{{id}}' :: bigint) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) 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), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (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) FROM "_sg_input" i) WHERE (("purchases"."id") = $2 :: bigint) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) 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), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (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) FROM "_sg_input" i) WHERE (("purchases"."id") = $2 :: bigint) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) 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 TestCompileUpdate/nestedUpdateOneToMany -WITH "_sg_input" AS (SELECT '{{data}}' :: 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") = '8' :: bigint) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (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) FROM "_sg_input" i) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id"= ((i.j->'product'->'where'->>'id'))::bigint) RETURNING "products".*) 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" +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") = '8' :: bigint) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (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) FROM "_sg_input" i) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id"= ((i.j->'product'->'where'->>'id'))::bigint) RETURNING "products".*) 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/nestedUpdateOneToOne -WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (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) FROM "_sg_input" i) WHERE (("products"."id") = '{{id}}' :: bigint) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) 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), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (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) FROM "_sg_input" i) WHERE (("products"."id") = $2 :: bigint) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) 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/nestedUpdateOneToManyWithConnect -WITH "_sg_input" AS (SELECT '{{data}}' :: 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") = '{{id}}' :: 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" +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 '{{data}}' :: 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") = '{{product_id}}' :: 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 '{{data}}' :: 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") = '{{product_id}}' :: 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 '{{data}}' :: 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") = '{{id}}' :: 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" +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) --- PASS: TestCompileUpdate/singleUpdate (0.00s) --- PASS: TestCompileUpdate/simpleUpdateWithPresets (0.00s) @@ -148,4 +151,4 @@ WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FR --- PASS: TestCompileUpdate/nestedUpdateOneToOneWithConnect (0.00s) --- PASS: TestCompileUpdate/nestedUpdateOneToOneWithDisconnect (0.00s) PASS -ok github.com/dosco/super-graph/core/internal/psql 0.306s +ok github.com/dosco/super-graph/core/internal/psql (cached) diff --git a/core/internal/psql/update.go b/core/internal/psql/update.go index da516ff..21395d9 100644 --- a/core/internal/psql/update.go +++ b/core/internal/psql/update.go @@ -10,8 +10,8 @@ import ( "github.com/dosco/super-graph/core/internal/util" ) -func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer, - vars Variables, ti *DBTableInfo) (uint32, error) { +func (c *compilerContext) renderUpdate( + w io.Writer, qc *qcode.QCode, vars Variables, ti *DBTableInfo) (uint32, error) { update, ok := vars[qc.ActionVar] if !ok { @@ -21,9 +21,10 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer, return 0, fmt.Errorf("variable '%s' is empty", qc.ActionVar) } - io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`) - io.WriteString(c.w, qc.ActionVar) - io.WriteString(c.w, `}}' :: json AS j)`) + io.WriteString(c.w, `WITH "_sg_input" AS (SELECT `) + c.renderValueExp(Param{Name: qc.ActionVar, Type: "json"}) + // io.WriteString(c.w, qc.ActionVar) + io.WriteString(c.w, ` :: json AS j)`) st := util.NewStack() st.Push(kvitem{_type: itemUpdate, key: ti.Name, val: update, ti: ti}) @@ -84,11 +85,11 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re io.WriteString(w, `UPDATE `) quoted(w, ti.Name) io.WriteString(w, ` SET (`) - renderInsertUpdateColumns(w, qc, jt, ti, sk, false) + c.renderInsertUpdateColumns(qc, jt, ti, sk, false) renderNestedUpdateRelColumns(w, item.kvitem, false) io.WriteString(w, `) = (SELECT `) - renderInsertUpdateColumns(w, qc, jt, ti, sk, true) + c.renderInsertUpdateColumns(qc, jt, ti, sk, true) renderNestedUpdateRelColumns(w, item.kvitem, true) io.WriteString(w, ` FROM "_sg_input" i`) @@ -122,7 +123,7 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re } else { if qc.Selects[0].Where != nil { - io.WriteString(w, ` WHERE `) + io.WriteString(w, `WHERE `) if err := c.renderWhere(&qc.Selects[0], ti); err != nil { return err } @@ -197,8 +198,9 @@ func renderNestedUpdateRelTables(w io.Writer, item kvitem) error { return nil } -func (c *compilerContext) renderDelete(qc *qcode.QCode, w io.Writer, - vars Variables, ti *DBTableInfo) (uint32, error) { +func (c *compilerContext) renderDelete( + w io.Writer, qc *qcode.QCode, vars Variables, ti *DBTableInfo) (uint32, error) { + root := &qc.Selects[0] io.WriteString(c.w, `WITH `) diff --git a/core/internal/psql/update_test.go b/core/internal/psql/update_test.go index f410a18..c61aa86 100644 --- a/core/internal/psql/update_test.go +++ b/core/internal/psql/update_test.go @@ -223,7 +223,7 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) { // } // }` -// sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 2) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` +// sql := `WITH "_sg_input" AS (SELECT $1 :: json AS j), "users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 2) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` // vars := map[string]json.RawMessage{ // "data": json.RawMessage(`{ diff --git a/core/internal/psql/utils.go b/core/internal/psql/utils.go deleted file mode 100644 index 195d294..0000000 --- a/core/internal/psql/utils.go +++ /dev/null @@ -1,13 +0,0 @@ -package psql - -import "regexp" - -func NewVariables(varlist map[string]string) map[string]string { - re := regexp.MustCompile(`(?mi)\$([a-zA-Z0-9_.]+)`) - vars := make(map[string]string, len(varlist)) - - for k, v := range varlist { - vars[k] = re.ReplaceAllString(v, `{{$1}}`) - } - return vars -} diff --git a/core/internal/qcode/config.go b/core/internal/qcode/config.go index 7082226..a13f5b2 100644 --- a/core/internal/qcode/config.go +++ b/core/internal/qcode/config.go @@ -1,7 +1,6 @@ package qcode import ( - "regexp" "sort" "strings" ) @@ -46,8 +45,7 @@ type TRConfig struct { } type trval struct { - readOnly bool - query struct { + query struct { limit string fil *Exp filNU bool @@ -132,12 +130,3 @@ func mapToList(m map[string]string) []string { sort.Strings(list) return list } - -var varRe = regexp.MustCompile(`\$([a-zA-Z0-9_]+)`) - -func parsePresets(m map[string]string) map[string]string { - for k, v := range m { - m[k] = varRe.ReplaceAllString(v, `{{$1}}`) - } - return m -} diff --git a/core/internal/qcode/qcode.go b/core/internal/qcode/qcode.go index d5ee494..561c39e 100644 --- a/core/internal/qcode/qcode.go +++ b/core/internal/qcode/qcode.go @@ -170,7 +170,6 @@ const ( ) type Compiler struct { - db bool // default block tables if not defined in anon role tr map[string]map[string]*trval bl map[string]struct{} } @@ -227,7 +226,7 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error { return err } trv.insert.cols = listToMap(trc.Insert.Columns) - trv.insert.psmap = parsePresets(trc.Insert.Presets) + trv.insert.psmap = trc.Insert.Presets trv.insert.pslist = mapToList(trv.insert.psmap) trv.insert.block = trc.Insert.Block @@ -237,7 +236,7 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error { return err } trv.update.cols = listToMap(trc.Update.Columns) - trv.update.psmap = parsePresets(trc.Update.Presets) + trv.update.psmap = trc.Update.Presets trv.update.pslist = mapToList(trv.update.psmap) trv.update.block = trc.Update.Block diff --git a/core/prepare.go b/core/prepare.go index ad236c9..d554dca 100644 --- a/core/prepare.go +++ b/core/prepare.go @@ -12,12 +12,10 @@ import ( "github.com/dosco/super-graph/core/internal/allow" "github.com/dosco/super-graph/core/internal/qcode" - "github.com/valyala/fasttemplate" ) type preparedItem struct { sd *sql.Stmt - args [][]byte st stmt roleArg bool } @@ -132,16 +130,13 @@ func (sg *SuperGraph) prepareStmt(item allow.Item) error { } func (sg *SuperGraph) prepare(ct context.Context, st []stmt, key string) error { - finalSQL, am := processTemplate(st[0].sql) - - sd, err := sg.db.PrepareContext(ct, finalSQL) + sd, err := sg.db.PrepareContext(ct, st[0].sql) if err != nil { - return fmt.Errorf("prepare failed: %v: %s", err, finalSQL) + return fmt.Errorf("prepare failed: %v: %s", err, st[0].sql) } sg.prepared[key] = &preparedItem{ sd: sd, - args: am, st: st[0], roleArg: len(st) > 1, } @@ -156,10 +151,11 @@ func (sg *SuperGraph) prepareRoleStmt(tx *sql.Tx) error { return nil } + rq := strings.ReplaceAll(sg.conf.RolesQuery, "$user_id", "$1") w := &bytes.Buffer{} io.WriteString(w, `SELECT (CASE WHEN EXISTS (`) - io.WriteString(w, sg.conf.RolesQuery) + io.WriteString(w, rq) io.WriteString(w, `) THEN `) io.WriteString(w, `(SELECT (CASE`) @@ -174,14 +170,12 @@ func (sg *SuperGraph) prepareRoleStmt(tx *sql.Tx) error { io.WriteString(w, `'`) } - io.WriteString(w, ` ELSE {{role}} END) FROM (`) + io.WriteString(w, ` ELSE $2 END) FROM (`) 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; `) - roleSQL, _ := processTemplate(w.String()) - - sg.getRole, err = tx.Prepare(roleSQL) + sg.getRole, err = tx.Prepare(w.String()) if err != nil { return err } @@ -189,36 +183,6 @@ func (sg *SuperGraph) prepareRoleStmt(tx *sql.Tx) error { return nil } -func processTemplate(tmpl string) (string, [][]byte) { - st := struct { - vmap map[string]int - am [][]byte - i int - }{ - vmap: make(map[string]int), - am: make([][]byte, 0, 5), - i: 0, - } - - execFunc := func(w io.Writer, tag string) (int, error) { - if n, ok := st.vmap[tag]; ok { - return w.Write([]byte(fmt.Sprintf("$%d", n))) - } - st.am = append(st.am, []byte(tag)) - st.i++ - st.vmap[tag] = st.i - return w.Write([]byte(fmt.Sprintf("$%d", st.i))) - } - - t1 := fasttemplate.New(tmpl, `'{{`, `}}'`) - ts1 := t1.ExecuteFuncString(execFunc) - - t2 := fasttemplate.New(ts1, `{{`, `}}`) - ts2 := t2.ExecuteFuncString(execFunc) - - return ts2, st.am -} - func (sg *SuperGraph) initAllowList() error { var ac allow.Config var err error diff --git a/core/remote.go b/core/remote.go index 6dbbd0c..d184394 100644 --- a/core/remote.go +++ b/core/remote.go @@ -21,7 +21,7 @@ func (sg *SuperGraph) execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([] // 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.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 diff --git a/docs/website/docs/home.md b/docs/website/docs/home.md index b68eeee..b50872d 100644 --- a/docs/website/docs/home.md +++ b/docs/website/docs/home.md @@ -157,7 +157,9 @@ func main() { } }` - res, err := sg.GraphQL(context.Background(), query, nil) + ctx = context.WithValue(ctx, core.UserIDKey, 1) + + res, err := sg.GraphQL(ctx, query, nil) if err != nil { log.Fatal(err) } diff --git a/docs/website/docs/intro.md b/docs/website/docs/intro.md index fe2cf00..9453c55 100644 --- a/docs/website/docs/intro.md +++ b/docs/website/docs/intro.md @@ -25,6 +25,8 @@ Super Graph has a rich feature set like integrating with your existing Ruby on R - Fuzz tested for security - Database migrations tool - Database seeding tool +- Works with Postgres and Yugabyte DB +- OpenCensus Support: Zipkin, Prometheus, X-Ray, Stackdriver ## Try the demo app diff --git a/go.mod b/go.mod index d6eacb8..405b913 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.6.3 github.com/stretchr/testify v1.5.1 - github.com/valyala/fasttemplate v1.1.0 go.opencensus.io v0.22.3 go.uber.org/zap v1.14.1 golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 diff --git a/go.sum b/go.sum index 8bbda4e..8b04d78 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,7 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= @@ -131,6 +132,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -142,7 +144,9 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -369,8 +373,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= -github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= diff --git a/internal/serv/cmd_migrate.go b/internal/serv/cmd_migrate.go index 5795cf6..d61299b 100644 --- a/internal/serv/cmd_migrate.go +++ b/internal/serv/cmd_migrate.go @@ -298,9 +298,9 @@ func ExtractErrorLine(source string, position int) (ErrorLineExtract, error) { func getMigrationVars() map[string]interface{} { return map[string]interface{}{ - "app_name": strings.Title(conf.AppName), - "app_name_slug": strings.ToLower(strings.Replace(conf.AppName, " ", "_", -1)), - "env": strings.ToLower(os.Getenv("GO_ENV")), + "AppName": strings.Title(conf.AppName), + "AppNameSlug": strings.ToLower(strings.Replace(conf.AppName, " ", "_", -1)), + "Env": strings.ToLower(os.Getenv("GO_ENV")), } } diff --git a/internal/serv/cmd_new.go b/internal/serv/cmd_new.go index fd4bfbf..05e7bb7 100644 --- a/internal/serv/cmd_new.go +++ b/internal/serv/cmd_new.go @@ -2,8 +2,7 @@ package serv import ( "bytes" - "fmt" - "io" + "html/template" "io/ioutil" "os" "path" @@ -11,7 +10,6 @@ import ( rice "github.com/GeertJohan/go.rice" "github.com/spf13/cobra" - "github.com/valyala/fasttemplate" ) func cmdNew(cmd *cobra.Command, args []string) { @@ -21,8 +19,8 @@ func cmdNew(cmd *cobra.Command, args []string) { } tmpl := newTempl(map[string]string{ - "app_name": strings.Title(strings.Join(args, " ")), - "app_name_slug": strings.ToLower(strings.Join(args, "_")), + "AppName": strings.Title(strings.Join(args, " ")), + "AppNameSlug": strings.ToLower(strings.Join(args, "_")), }) // Create app folder and add relevant files @@ -121,19 +119,16 @@ func newTempl(data map[string]string) *Templ { func (t *Templ) get(name string) ([]byte, error) { v := t.MustString(name) b := bytes.Buffer{} - tmpl := fasttemplate.New(v, "{%", "%}") - - _, err := tmpl.ExecuteFunc(&b, func(w io.Writer, tag string) (int, error) { - if val, ok := t.data[strings.TrimSpace(tag)]; ok { - return w.Write([]byte(val)) - } - return 0, fmt.Errorf("unknown template variable '%s'", tag) - }) + tmpl, err := template.New(name).Parse(v) if err != nil { return nil, err } + if err := tmpl.Execute(&b, t.data); err != nil { + return nil, err + } + return b.Bytes(), nil } diff --git a/internal/serv/rice-box.go b/internal/serv/rice-box.go index 4f83e68..1f8f03c 100644 --- a/internal/serv/rice-box.go +++ b/internal/serv/rice-box.go @@ -24,27 +24,27 @@ func init() { } file4 := &embedded.EmbeddedFile{ Filename: "cloudbuild.yaml", - FileModTime: time.Unix(1589749247, 0), + FileModTime: time.Unix(1590522003, 0), - Content: string("steps:\n # Build image with tag 'latest'\n - name: \"gcr.io/cloud-builders/docker\"\n args:\n [\n \"build\",\n \"--tag\",\n \"gcr.io/$PROJECT_ID/{% app_name_slug %}:latest\",\n \"--build-arg\",\n \"GO_ENV=production\",\n \".\",\n ]\n\n # Push new image to Google Container Registry\n - name: \"gcr.io/cloud-builders/docker\"\n args: [\"push\", \"gcr.io/$PROJECT_ID/{% app_name_slug %}:latest\"]\n\n # Deploy image to Cloud Run\n - name: \"gcr.io/cloud-builders/gcloud\"\n args:\n [\n \"run\",\n \"deploy\",\n \"data\",\n \"--image\",\n \"gcr.io/$PROJECT_ID/{% app_name_slug %}:latest\",\n \"--add-cloudsql-instances\",\n \"$PROJECT_ID:$REGION:{% app_name_slug %}_production\",\n \"--region\",\n \"$REGION\",\n \"--platform\",\n \"managed\",\n \"--update-env-vars\",\n \"GO_ENV=production,SG_DATABASE_HOST=/cloudsql/$PROJECT_ID:$REGION:{% app_name_slug %}_production,SECRETS_FILE=prod.secrets.yml\",\n \"--port\",\n \"8080\",\n \"--service-account\",\n \"$SERVICE_ACCOUNT\",\n \"--allow-unauthenticated\",\n \"--verbosity\",\n \"debug\",\n ]\n"), + Content: string("steps:\n # Build image with tag 'latest'\n - name: \"gcr.io/cloud-builders/docker\"\n args:\n [\n \"build\",\n \"--tag\",\n \"gcr.io/$PROJECT_ID/{{- .AppNameSlug -}}:latest\",\n \"--build-arg\",\n \"GO_ENV=production\",\n \".\",\n ]\n\n # Push new image to Google Container Registry\n - name: \"gcr.io/cloud-builders/docker\"\n args: [\"push\", \"gcr.io/$PROJECT_ID/{{- .AppNameSlug -}}:latest\"]\n\n # Deploy image to Cloud Run\n - name: \"gcr.io/cloud-builders/gcloud\"\n args:\n [\n \"run\",\n \"deploy\",\n \"data\",\n \"--image\",\n \"gcr.io/$PROJECT_ID/{{- .AppNameSlug -}}:latest\",\n \"--add-cloudsql-instances\",\n \"$PROJECT_ID:$REGION:{{- .AppNameSlug -}}_production\",\n \"--region\",\n \"$REGION\",\n \"--platform\",\n \"managed\",\n \"--update-env-vars\",\n \"GO_ENV=production,SG_DATABASE_HOST=/cloudsql/$PROJECT_ID:$REGION:{{- .AppNameSlug -}}_production,SECRETS_FILE=prod.secrets.yml\",\n \"--port\",\n \"8080\",\n \"--service-account\",\n \"$SERVICE_ACCOUNT\",\n \"--allow-unauthenticated\",\n \"--verbosity\",\n \"debug\",\n ]\n"), } file5 := &embedded.EmbeddedFile{ Filename: "dev.yml", - FileModTime: time.Unix(1590248169, 0), + FileModTime: time.Unix(1590522219, 0), - Content: string("app_name: \"{% app_name %} Development\"\nhost_port: 0.0.0.0:8080\nweb_ui: true\n\n# debug, error, warn, info\nlog_level: \"info\"\n\n# enable or disable http compression (uses gzip)\nhttp_compress: true\n\n# When production mode is 'true' only queries \n# from the allow list are permitted.\n# When it's 'false' all queries are saved to the\n# the allow list in ./config/allow.list\nproduction: false\n\n# Throw a 401 on auth failure for queries that need auth\nauth_fail_block: false\n\n# Latency tracing for database queries and remote joins\n# the resulting latency information is returned with the\n# response\nenable_tracing: true\n\n# Watch the config folder and reload Super Graph\n# with the new configs when a change is detected\nreload_on_config_change: true\n\n# File that points to the database seeding script\n# seed_file: seed.js\n\n# Path pointing to where the migrations can be found\n# this must be a relative path under the config path\nmigrations_path: ./migrations\n\n# Secret key for general encryption operations like \n# encrypting the cursor data\nsecret_key: supercalifajalistics\n\n# CORS: A list of origins a cross-domain request can be executed from. \n# If the special * value is present in the list, all origins will be allowed. \n# An origin may contain a wildcard (*) to replace 0 or more \n# characters (i.e.: http://*.domain.com).\ncors_allowed_origins: [\"*\"]\n\n# Debug Cross Origin Resource Sharing requests\ncors_debug: false\n\n# Default API path prefix is /api you can change it if you like\n# api_path: \"/data\"\n\n# Cache-Control header can help cache queries if your CDN supports cache-control \n# on POST requests (does not work with not mutations) \n# cache_control: \"public, max-age=300, s-maxage=600\"\n\n# Postgres related environment Variables\n# SG_DATABASE_HOST\n# SG_DATABASE_PORT\n# SG_DATABASE_USER\n# SG_DATABASE_PASSWORD\n\n# Auth related environment Variables\n# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE\n# SG_AUTH_RAILS_REDIS_URL\n# SG_AUTH_RAILS_REDIS_PASSWORD\n# SG_AUTH_JWT_PUBLIC_KEY_FILE\n\n# inflections:\n# person: people\n# sheep: sheep\n\n# open opencensus tracing and metrics\n# telemetry:\n# debug: true\n# metrics:\n# exporter: \"prometheus\"\n# tracing:\n# exporter: \"zipkin\"\n# endpoint: \"http://zipkin:9411/api/v2/spans\"\n# sample: 0.6\n\nauth:\n # Can be 'rails', 'jwt' or 'header'\n type: rails\n cookie: _{% app_name_slug %}_session\n\n # Comment this out if you want to disable setting\n # the user_id via a header for testing. \n # Disable in production\n creds_in_header: true\n\n rails:\n # Rails version this is used for reading the\n # various cookies formats.\n version: 5.2\n\n # Found in 'Rails.application.config.secret_key_base'\n secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566\n\n # Remote cookie store. (memcache or redis)\n # url: redis://redis:6379\n # password: \"\"\n # max_idle: 80\n # max_active: 12000\n\n # In most cases you don't need these\n # salt: \"encrypted cookie\"\n # sign_salt: \"signed encrypted cookie\"\n # auth_salt: \"authenticated encrypted cookie\"\n\n # jwt:\n # provider: auth0\n # secret: abc335bfcfdb04e50db5bb0a4d67ab9\n # public_key_file: /secrets/public_key.pem\n # public_key_type: ecdsa #rsa\n\n # header:\n # name: dnt\n # exists: true\n # value: localhost:8080\n\n# You can add additional named auths to use with actions\n# In this example actions using this auth can only be\n# called from the Google Appengine Cron service that\n# sets a special header to all it's requests\nauths:\n - name: from_taskqueue\n type: header\n header:\n name: X-Appengine-Cron\n exists: true\n\ndatabase:\n type: postgres\n host: db\n port: 5432\n dbname: {% app_name_slug %}_development\n user: postgres\n password: postgres\n\n #schema: \"public\"\n #pool_size: 10\n #max_retries: 0\n #log_level: \"debug\"\n\n # Set session variable \"user.id\" to the user id\n # Enable this if you need the user id in triggers, etc\n set_user_id: false\n\n # database ping timeout is used for db health checking\n ping_timeout: 1m\n\n # Set up an secure tls encrypted db connection\n enable_tls: false\n\n # Required for tls. For example with Google Cloud SQL it's\n # :\"\n # server_name: blah\n\n # Required for tls. Can be a file path or the contents of the pem file\n # server_cert: ./server-ca.pem\n\n # Required for tls. Can be a file path or the contents of the pem file\n # client_cert: ./client-cert.pem\n\n # Required for tls. Can be a file path or the contents of the pem file\n # client_key: ./client-key.pem\n\n# Define additional variables here to be used with filters\nvariables:\n #admin_account_id: \"5\"\n admin_account_id: \"sql:select id from users where admin = true limit 1\"\n\n\n# Field and table names that you wish to block\nblocklist:\n - ar_internal_metadata\n - schema_migrations\n - secret\n - password\n - encrypted\n - token\n\n# Create custom actions with their own api endpoints\n# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users\n# A request to this url will execute the configured SQL query\n# which in this case refreshes a materialized view in the database.\n# The auth_name is from one of the configured auths\nactions:\n - name: refresh_leaderboard_users\n sql: REFRESH MATERIALIZED VIEW CONCURRENTLY \"leaderboard_users\"\n auth_name: from_taskqueue\n\ntables:\n - name: customers\n remotes:\n - name: payments\n id: stripe_id\n url: http://rails_app:3000/stripe/$id\n path: data\n # debug: true\n pass_headers: \n - cookie\n set_headers:\n - name: Host\n value: 0.0.0.0\n # - name: Authorization\n # value: Bearer \n\n - # You can create new fields that have a\n # real db table backing them\n name: me\n table: users\n\n\n#roles_query: \"SELECT\u00a0* FROM users WHERE id = $user_id\"\n\nroles:\n - name: anon\n tables:\n - name: users\n query:\n limit: 10\n\n - name: user\n tables:\n - name: users\n query:\n filters: [\"{ id: { _eq: $user_id } }\"]\n\n - name: products\n query:\n limit: 50\n filters: [\"{ user_id: { eq: $user_id } }\"]\n disable_functions: false\n\n insert:\n filters: [\"{ user_id: { eq: $user_id } }\"]\n presets:\n - user_id: \"$user_id\"\n - created_at: \"now\"\n \n update:\n filters: [\"{ user_id: { eq: $user_id } }\"]\n presets:\n - updated_at: \"now\"\n\n delete:\n block: true\n\n # - name: admin\n # match: id = 1000\n # tables:\n # - name: users\n # filters: []\n"), + Content: string("app_name: \"{{- .AppName }} Development\"\nhost_port: 0.0.0.0:8080\nweb_ui: true\n\n# debug, error, warn, info\nlog_level: \"info\"\n\n# enable or disable http compression (uses gzip)\nhttp_compress: true\n\n# When production mode is 'true' only queries \n# from the allow list are permitted.\n# When it's 'false' all queries are saved to the\n# the allow list in ./config/allow.list\nproduction: false\n\n# Throw a 401 on auth failure for queries that need auth\nauth_fail_block: false\n\n# Latency tracing for database queries and remote joins\n# the resulting latency information is returned with the\n# response\nenable_tracing: true\n\n# Watch the config folder and reload Super Graph\n# with the new configs when a change is detected\nreload_on_config_change: true\n\n# File that points to the database seeding script\n# seed_file: seed.js\n\n# Path pointing to where the migrations can be found\n# this must be a relative path under the config path\nmigrations_path: ./migrations\n\n# Secret key for general encryption operations like \n# encrypting the cursor data\nsecret_key: supercalifajalistics\n\n# CORS: A list of origins a cross-domain request can be executed from. \n# If the special * value is present in the list, all origins will be allowed. \n# An origin may contain a wildcard (*) to replace 0 or more \n# characters (i.e.: http://*.domain.com).\ncors_allowed_origins: [\"*\"]\n\n# Debug Cross Origin Resource Sharing requests\ncors_debug: false\n\n# Default API path prefix is /api you can change it if you like\n# api_path: \"/data\"\n\n# Cache-Control header can help cache queries if your CDN supports cache-control \n# on POST requests (does not work with not mutations) \n# cache_control: \"public, max-age=300, s-maxage=600\"\n\n# Postgres related environment Variables\n# SG_DATABASE_HOST\n# SG_DATABASE_PORT\n# SG_DATABASE_USER\n# SG_DATABASE_PASSWORD\n\n# Auth related environment Variables\n# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE\n# SG_AUTH_RAILS_REDIS_URL\n# SG_AUTH_RAILS_REDIS_PASSWORD\n# SG_AUTH_JWT_PUBLIC_KEY_FILE\n\n# inflections:\n# person: people\n# sheep: sheep\n\n# open opencensus tracing and metrics\n# telemetry:\n# debug: true\n# metrics:\n# exporter: \"prometheus\"\n# tracing:\n# exporter: \"zipkin\"\n# endpoint: \"http://zipkin:9411/api/v2/spans\"\n# sample: 0.6\n\nauth:\n # Can be 'rails', 'jwt' or 'header'\n type: rails\n cookie: _{{- .AppNameSlug -}}_session\n\n # Comment this out if you want to disable setting\n # the user_id via a header for testing. \n # Disable in production\n creds_in_header: true\n\n rails:\n # Rails version this is used for reading the\n # various cookies formats.\n version: 5.2\n\n # Found in 'Rails.application.config.secret_key_base'\n secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566\n\n # Remote cookie store. (memcache or redis)\n # url: redis://redis:6379\n # password: \"\"\n # max_idle: 80\n # max_active: 12000\n\n # In most cases you don't need these\n # salt: \"encrypted cookie\"\n # sign_salt: \"signed encrypted cookie\"\n # auth_salt: \"authenticated encrypted cookie\"\n\n # jwt:\n # provider: auth0\n # secret: abc335bfcfdb04e50db5bb0a4d67ab9\n # public_key_file: /secrets/public_key.pem\n # public_key_type: ecdsa #rsa\n\n # header:\n # name: dnt\n # exists: true\n # value: localhost:8080\n\n# You can add additional named auths to use with actions\n# In this example actions using this auth can only be\n# called from the Google Appengine Cron service that\n# sets a special header to all it's requests\nauths:\n - name: from_taskqueue\n type: header\n header:\n name: X-Appengine-Cron\n exists: true\n\ndatabase:\n type: postgres\n host: db\n port: 5432\n dbname: {{- .AppNameSlug -}}_development\n user: postgres\n password: postgres\n\n #schema: \"public\"\n #pool_size: 10\n #max_retries: 0\n #log_level: \"debug\"\n\n # Set session variable \"user.id\" to the user id\n # Enable this if you need the user id in triggers, etc\n set_user_id: false\n\n # database ping timeout is used for db health checking\n ping_timeout: 1m\n\n # Set up an secure tls encrypted db connection\n enable_tls: false\n\n # Required for tls. For example with Google Cloud SQL it's\n # :\"\n # server_name: blah\n\n # Required for tls. Can be a file path or the contents of the pem file\n # server_cert: ./server-ca.pem\n\n # Required for tls. Can be a file path or the contents of the pem file\n # client_cert: ./client-cert.pem\n\n # Required for tls. Can be a file path or the contents of the pem file\n # client_key: ./client-key.pem\n\n# Define additional variables here to be used with filters\nvariables:\n #admin_account_id: \"5\"\n admin_account_id: \"sql:select id from users where admin = true limit 1\"\n\n\n# Field and table names that you wish to block\nblocklist:\n - ar_internal_metadata\n - schema_migrations\n - secret\n - password\n - encrypted\n - token\n\n# Create custom actions with their own api endpoints\n# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users\n# A request to this url will execute the configured SQL query\n# which in this case refreshes a materialized view in the database.\n# The auth_name is from one of the configured auths\nactions:\n - name: refresh_leaderboard_users\n sql: REFRESH MATERIALIZED VIEW CONCURRENTLY \"leaderboard_users\"\n auth_name: from_taskqueue\n\ntables:\n - name: customers\n remotes:\n - name: payments\n id: stripe_id\n url: http://rails_app:3000/stripe/$id\n path: data\n # debug: true\n pass_headers: \n - cookie\n set_headers:\n - name: Host\n value: 0.0.0.0\n # - name: Authorization\n # value: Bearer \n\n - # You can create new fields that have a\n # real db table backing them\n name: me\n table: users\n\n\n#roles_query: \"SELECT\u00a0* FROM users WHERE id = $user_id\"\n\nroles:\n - name: anon\n tables:\n - name: users\n query:\n limit: 10\n\n - name: user\n tables:\n - name: users\n query:\n filters: [\"{ id: { _eq: $user_id } }\"]\n\n - name: products\n query:\n limit: 50\n filters: [\"{ user_id: { eq: $user_id } }\"]\n disable_functions: false\n\n insert:\n filters: [\"{ user_id: { eq: $user_id } }\"]\n presets:\n - user_id: \"$user_id\"\n - created_at: \"now\"\n \n update:\n filters: [\"{ user_id: { eq: $user_id } }\"]\n presets:\n - updated_at: \"now\"\n\n delete:\n block: true\n\n # - name: admin\n # match: id = 1000\n # tables:\n # - name: users\n # filters: []\n"), } file6 := &embedded.EmbeddedFile{ Filename: "docker-compose.yml", - FileModTime: time.Unix(1582491326, 0), + FileModTime: time.Unix(1590536218, 0), - Content: string("version: '3.4'\nservices:\n # Postgres DB\n db:\n image: postgres:12\n environment:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n ports:\n - \"5432:5432\"\n\n # Yugabyte DB\n # yb-master: \n # image: yugabytedb/yugabyte:latest \n # container_name: yb-master-n1 \n # command: [ \"/home/yugabyte/bin/yb-master\", \n # \"--fs_data_dirs=/mnt/disk0,/mnt/disk1\", \n # \"--master_addresses=yb-master-n1:7100\", \n # \"--replication_factor=1\", \n # \"--enable_ysql=true\"] \n # ports: \n # - \"7000:7000\" \n # environment: \n # SERVICE_7000_NAME: yb-master \n \n # db: \n # image: yugabytedb/yugabyte:latest \n # container_name: yb-tserver-n1 \n # command: [ \"/home/yugabyte/bin/yb-tserver\", \n # \"--fs_data_dirs=/mnt/disk0,/mnt/disk1\", \n # \"--start_pgsql_proxy\", \n # \"--tserver_master_addrs=yb-master-n1:7100\"] \n # ports: \n # - \"9042:9042\" \n # - \"6379:6379\" \n # - \"5433:5433\" \n # - \"9000:9000\" \n # environment: \n # SERVICE_5433_NAME: ysql \n # SERVICE_9042_NAME: ycql \n # SERVICE_6379_NAME: yedis \n # SERVICE_9000_NAME: yb-tserver \n # depends_on: \n # - yb-master\n\n {% app_name_slug %}_api:\n image: dosco/super-graph:latest\n environment:\n GO_ENV: \"development\"\n # Uncomment below for Yugabyte DB\n # SG_DATABASE_PORT: 5433\n # SG_DATABASE_USER: yugabyte\n # SG_DATABASE_PASSWORD: yugabyte\n volumes:\n - ./config:/config\n ports:\n - \"8080:8080\"\n depends_on:\n - db"), + Content: string("version: '3.4'\nservices:\n # Postgres DB\n db:\n image: postgres:12\n environment:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n ports:\n - \"5432:5432\"\n\n {{ .AppNameSlug -}}_api:\n image: dosco/super-graph:latest\n environment:\n GO_ENV: \"development\"\n volumes:\n - ./config:/config\n ports:\n - \"8080:8080\"\n depends_on:\n - db"), } file7 := &embedded.EmbeddedFile{ Filename: "prod.yml", - FileModTime: time.Unix(1590248179, 0), + FileModTime: time.Unix(1590522139, 0), - Content: string("# Inherit config from this other config file\n# so I only need to overwrite some values\ninherits: dev\n\napp_name: \"{% app_name %} Production\"\nhost_port: 0.0.0.0:8080\nweb_ui: false\n\n# debug, error, warn, info\nlog_level: \"warn\"\n\n# enable or disable http compression (uses gzip)\nhttp_compress: true\n\n# When production mode is 'true' only queries \n# from the allow list are permitted.\n# When it's 'false' all queries are saved to the\n# the allow list in ./config/allow.list\nproduction: true\n\n# Throw a 401 on auth failure for queries that need auth\nauth_fail_block: true\n\n# Latency tracing for database queries and remote joins\n# the resulting latency information is returned with the\n# response\nenable_tracing: false\n\n# Watch the config folder and reload Super Graph\n# with the new configs when a change is detected\nreload_on_config_change: false\n\n# File that points to the database seeding script\n# seed_file: seed.js\n\n# Path pointing to where the migrations can be found\n# migrations_path: ./migrations\n\n# Secret key for general encryption operations like \n# encrypting the cursor data\n# secret_key: supercalifajalistics\n\n# CORS: A list of origins a cross-domain request can be executed from. \n# If the special * value is present in the list, all origins will be allowed. \n# An origin may contain a wildcard (*) to replace 0 or more \n# characters (i.e.: http://*.domain.com).\n# cors_allowed_origins: [\"*\"]\n\n# Debug Cross Origin Resource Sharing requests\n# cors_debug: false\n\n# Default API path prefix is /api you can change it if you like\n# api_path: \"/data\"\n\n# Cache-Control header can help cache queries if your CDN supports cache-control \n# on POST requests (does not work with not mutations) \n# cache_control: \"public, max-age=300, s-maxage=600\"\n\n# Postgres related environment Variables\n# SG_DATABASE_HOST\n# SG_DATABASE_PORT\n# SG_DATABASE_USER\n# SG_DATABASE_PASSWORD\n\n# Auth related environment Variables\n# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE\n# SG_AUTH_RAILS_REDIS_URL\n# SG_AUTH_RAILS_REDIS_PASSWORD\n# SG_AUTH_JWT_PUBLIC_KEY_FILE\n\n# open opencensus tracing and metrics\n# telemetry:\n# debug: false\n# metrics:\n# exporter: \"prometheus\"\n# tracing:\n# exporter: \"zipkin\"\n# endpoint: \"http://zipkin:9411/api/v2/spans\"\n# sample: 0.6\n\ndatabase:\n type: postgres\n host: db\n port: 5432\n dbname: {% app_name_slug %}_production\n user: postgres\n password: postgres\n #pool_size: 10\n #max_retries: 0\n #log_level: \"debug\" \n\n # Set session variable \"user.id\" to the user id\n # Enable this if you need the user id in triggers, etc\n set_user_id: false\n\n # database ping timeout is used for db health checking\n ping_timeout: 5m\n\n # Set up an secure tls encrypted db connection\n enable_tls: false\n\n # Required for tls. For example with Google Cloud SQL it's\n # :\"\n # server_name: blah\n\n # Required for tls. Can be a file path or the contents of the pem file\n # server_cert: ./server-ca.pem\n\n # Required for tls. Can be a file path or the contents of the pem file\n # client_cert: ./client-cert.pem\n\n # Required for tls. Can be a file path or the contents of the pem file\n # client_key: ./client-key.pem"), + Content: string("# Inherit config from this other config file\n# so I only need to overwrite some values\ninherits: dev\n\napp_name: \"{{- .AppName }} Production\"\nhost_port: 0.0.0.0:8080\nweb_ui: false\n\n# debug, error, warn, info\nlog_level: \"warn\"\n\n# enable or disable http compression (uses gzip)\nhttp_compress: true\n\n# When production mode is 'true' only queries \n# from the allow list are permitted.\n# When it's 'false' all queries are saved to the\n# the allow list in ./config/allow.list\nproduction: true\n\n# Throw a 401 on auth failure for queries that need auth\nauth_fail_block: true\n\n# Latency tracing for database queries and remote joins\n# the resulting latency information is returned with the\n# response\nenable_tracing: false\n\n# Watch the config folder and reload Super Graph\n# with the new configs when a change is detected\nreload_on_config_change: false\n\n# File that points to the database seeding script\n# seed_file: seed.js\n\n# Path pointing to where the migrations can be found\n# migrations_path: ./migrations\n\n# Secret key for general encryption operations like \n# encrypting the cursor data\n# secret_key: supercalifajalistics\n\n# CORS: A list of origins a cross-domain request can be executed from. \n# If the special * value is present in the list, all origins will be allowed. \n# An origin may contain a wildcard (*) to replace 0 or more \n# characters (i.e.: http://*.domain.com).\n# cors_allowed_origins: [\"*\"]\n\n# Debug Cross Origin Resource Sharing requests\n# cors_debug: false\n\n# Default API path prefix is /api you can change it if you like\n# api_path: \"/data\"\n\n# Cache-Control header can help cache queries if your CDN supports cache-control \n# on POST requests (does not work with not mutations) \n# cache_control: \"public, max-age=300, s-maxage=600\"\n\n# Postgres related environment Variables\n# SG_DATABASE_HOST\n# SG_DATABASE_PORT\n# SG_DATABASE_USER\n# SG_DATABASE_PASSWORD\n\n# Auth related environment Variables\n# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE\n# SG_AUTH_RAILS_REDIS_URL\n# SG_AUTH_RAILS_REDIS_PASSWORD\n# SG_AUTH_JWT_PUBLIC_KEY_FILE\n\n# open opencensus tracing and metrics\n# telemetry:\n# debug: false\n# metrics:\n# exporter: \"prometheus\"\n# tracing:\n# exporter: \"zipkin\"\n# endpoint: \"http://zipkin:9411/api/v2/spans\"\n# sample: 0.6\n\ndatabase:\n type: postgres\n host: db\n port: 5432\n dbname: {{- .AppNameSlug -}}_production\n user: postgres\n password: postgres\n #pool_size: 10\n #max_retries: 0\n #log_level: \"debug\" \n\n # Set session variable \"user.id\" to the user id\n # Enable this if you need the user id in triggers, etc\n set_user_id: false\n\n # database ping timeout is used for db health checking\n ping_timeout: 5m\n\n # Set up an secure tls encrypted db connection\n enable_tls: false\n\n # Required for tls. For example with Google Cloud SQL it's\n # :\"\n # server_name: blah\n\n # Required for tls. Can be a file path or the contents of the pem file\n # server_cert: ./server-ca.pem\n\n # Required for tls. Can be a file path or the contents of the pem file\n # client_cert: ./client-cert.pem\n\n # Required for tls. Can be a file path or the contents of the pem file\n # client_key: ./client-key.pem"), } file8 := &embedded.EmbeddedFile{ Filename: "seed.js", diff --git a/internal/serv/tmpl/cloudbuild.yaml b/internal/serv/tmpl/cloudbuild.yaml index a4f9216..9931a0e 100644 --- a/internal/serv/tmpl/cloudbuild.yaml +++ b/internal/serv/tmpl/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: [ "build", "--tag", - "gcr.io/$PROJECT_ID/{% app_name_slug %}:latest", + "gcr.io/$PROJECT_ID/{{- .AppNameSlug -}}:latest", "--build-arg", "GO_ENV=production", ".", @@ -13,7 +13,7 @@ steps: # Push new image to Google Container Registry - name: "gcr.io/cloud-builders/docker" - args: ["push", "gcr.io/$PROJECT_ID/{% app_name_slug %}:latest"] + args: ["push", "gcr.io/$PROJECT_ID/{{- .AppNameSlug -}}:latest"] # Deploy image to Cloud Run - name: "gcr.io/cloud-builders/gcloud" @@ -23,15 +23,15 @@ steps: "deploy", "data", "--image", - "gcr.io/$PROJECT_ID/{% app_name_slug %}:latest", + "gcr.io/$PROJECT_ID/{{- .AppNameSlug -}}:latest", "--add-cloudsql-instances", - "$PROJECT_ID:$REGION:{% app_name_slug %}_production", + "$PROJECT_ID:$REGION:{{- .AppNameSlug -}}_production", "--region", "$REGION", "--platform", "managed", "--update-env-vars", - "GO_ENV=production,SG_DATABASE_HOST=/cloudsql/$PROJECT_ID:$REGION:{% app_name_slug %}_production,SECRETS_FILE=prod.secrets.yml", + "GO_ENV=production,SG_DATABASE_HOST=/cloudsql/$PROJECT_ID:$REGION:{{- .AppNameSlug -}}_production,SECRETS_FILE=prod.secrets.yml", "--port", "8080", "--service-account", diff --git a/internal/serv/tmpl/dev.yml b/internal/serv/tmpl/dev.yml index bfa164f..688bb5c 100644 --- a/internal/serv/tmpl/dev.yml +++ b/internal/serv/tmpl/dev.yml @@ -1,4 +1,4 @@ -app_name: "{% app_name %} Development" +app_name: "{{- .AppName }} Development" host_port: 0.0.0.0:8080 web_ui: true @@ -82,7 +82,7 @@ cors_debug: false auth: # Can be 'rails', 'jwt' or 'header' type: rails - cookie: _{% app_name_slug %}_session + cookie: _{{- .AppNameSlug -}}_session # Comment this out if you want to disable setting # the user_id via a header for testing. @@ -134,7 +134,7 @@ database: type: postgres host: db port: 5432 - dbname: {% app_name_slug %}_development + dbname: {{- .AppNameSlug -}}_development user: postgres password: postgres diff --git a/internal/serv/tmpl/docker-compose.yml b/internal/serv/tmpl/docker-compose.yml index b523295..1274f43 100644 --- a/internal/serv/tmpl/docker-compose.yml +++ b/internal/serv/tmpl/docker-compose.yml @@ -9,48 +9,10 @@ services: ports: - "5432:5432" - # Yugabyte DB - # yb-master: - # image: yugabytedb/yugabyte:latest - # container_name: yb-master-n1 - # command: [ "/home/yugabyte/bin/yb-master", - # "--fs_data_dirs=/mnt/disk0,/mnt/disk1", - # "--master_addresses=yb-master-n1:7100", - # "--replication_factor=1", - # "--enable_ysql=true"] - # ports: - # - "7000:7000" - # environment: - # SERVICE_7000_NAME: yb-master - - # db: - # image: yugabytedb/yugabyte:latest - # container_name: yb-tserver-n1 - # command: [ "/home/yugabyte/bin/yb-tserver", - # "--fs_data_dirs=/mnt/disk0,/mnt/disk1", - # "--start_pgsql_proxy", - # "--tserver_master_addrs=yb-master-n1:7100"] - # ports: - # - "9042:9042" - # - "6379:6379" - # - "5433:5433" - # - "9000:9000" - # environment: - # SERVICE_5433_NAME: ysql - # SERVICE_9042_NAME: ycql - # SERVICE_6379_NAME: yedis - # SERVICE_9000_NAME: yb-tserver - # depends_on: - # - yb-master - - {% app_name_slug %}_api: + {{ .AppNameSlug -}}_api: image: dosco/super-graph:latest environment: GO_ENV: "development" - # Uncomment below for Yugabyte DB - # SG_DATABASE_PORT: 5433 - # SG_DATABASE_USER: yugabyte - # SG_DATABASE_PASSWORD: yugabyte volumes: - ./config:/config ports: diff --git a/internal/serv/tmpl/prod.yml b/internal/serv/tmpl/prod.yml index 25d9551..a07cfd2 100644 --- a/internal/serv/tmpl/prod.yml +++ b/internal/serv/tmpl/prod.yml @@ -2,7 +2,7 @@ # so I only need to overwrite some values inherits: dev -app_name: "{% app_name %} Production" +app_name: "{{- .AppName }} Production" host_port: 0.0.0.0:8080 web_ui: false @@ -82,7 +82,7 @@ database: type: postgres host: db port: 5432 - dbname: {% app_name_slug %}_production + dbname: {{- .AppNameSlug -}}_production user: postgres password: postgres #pool_size: 10