Compare commits

..

22 Commits

Author SHA1 Message Date
99b37a9c50 Fix bug related to new Postgres docker image 2020-02-23 10:28:32 +05:30
7ec1f59224 Fix bug with cursors and multiple order by 2020-02-23 02:28:37 +05:30
d3ecb1d6cc Fix bug with multi root queries 2020-02-21 10:29:37 +05:30
aed4170e8e Fix bug with cursor filters 2020-02-20 22:53:29 +05:30
c33e93ab37 Add support for cursors with multiple order by clauses 2020-02-19 10:22:44 +05:30
3d3e5d9c2b Add Yugabyte to docs 2020-02-12 08:42:53 +05:30
67b4a4d945 Fix issue with cursor as a variable 2020-02-11 11:41:35 +05:30
7413813138 Add pagination using opaque cursors 2020-02-10 12:15:37 +05:30
12007db76e Add support for Yugabyte DB 2020-02-07 11:42:14 +05:30
c85d379fe2 Add ability to add comments to the allow list 2020-02-04 00:20:25 -05:00
62fd1eac55 Add named auth and the all new action endpoints 2020-02-03 01:21:07 -05:00
1a3d74e1ce Fix issues surfaced by the fuzzer 2020-02-02 01:43:09 -05:00
3a4d885987 Fix to ensure only named queries are saved to the allow list 2020-02-01 10:54:19 -05:00
3bd9b199dd Fix bug with connect / disconnect on array relationships 2020-01-31 00:19:38 -05:00
4ffa1483a4 Add ability to treat JSON/JSONB columns as tables 2020-01-28 00:26:53 -05:00
52f3b1c7a2 Add mutation support for connect / disconnect with array relationships 2020-01-26 01:10:54 -05:00
2d466bfb12 Add skip query selectors that require auth in anon role 2020-01-20 23:38:17 -05:00
a0b8907c3c Fix various json parsing and sql generation bugs 2020-01-19 03:12:51 -05:00
8097ca3b8f Fixes example steps (#33) 2020-01-18 16:44:16 -05:00
0e498b0e94 Fix order by with aliases bug 2020-01-17 09:35:14 -05:00
3eb5b83070 Fix invalid update sql bug 2020-01-17 00:48:17 -05:00
e3c94d17d1 Add corrupt query validation 2020-01-16 01:44:19 -05:00
70 changed files with 3786 additions and 2107 deletions

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ supergraph
crashers crashers
suppressions suppressions
release release
.gofuzz
*-fuzz.zip

View File

@ -48,6 +48,7 @@ This compiler is what sits at the heart of Super Graph with layers of useful fun
- Fuzz tested for security - Fuzz tested for security
- Database migrations tool - Database migrations tool
- Database seeding tool - Database seeding tool
- Works with Postgres and YugabyteDB
## Get started ## Get started

361
allow/allow.go Normal file
View File

@ -0,0 +1,361 @@
package allow
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"sort"
"strings"
)
const (
AL_QUERY int = iota + 1
AL_VARS
)
type Item struct {
Name string
key string
Query string
Vars json.RawMessage
Comment string
}
type List struct {
filepath string
saveChan chan Item
}
type Config struct {
CreateIfNotExists bool
Persist bool
}
func New(cpath string, conf Config) (*List, error) {
al := List{}
if len(cpath) != 0 {
fp := path.Join(cpath, "allow.list")
if _, err := os.Stat(fp); err == nil {
al.filepath = fp
} else if !os.IsNotExist(err) {
return nil, err
}
}
if len(al.filepath) == 0 {
fp := "./allow.list"
if _, err := os.Stat(fp); err == nil {
al.filepath = fp
} else if !os.IsNotExist(err) {
return nil, err
}
}
if len(al.filepath) == 0 {
fp := "./config/allow.list"
if _, err := os.Stat(fp); err == nil {
al.filepath = fp
} else if !os.IsNotExist(err) {
return nil, err
}
}
if len(al.filepath) == 0 {
if !conf.CreateIfNotExists {
return nil, errors.New("allow.list not found")
}
if len(cpath) == 0 {
al.filepath = "./config/allow.list"
} else {
al.filepath = path.Join(cpath, "allow.list")
}
}
var err error
if conf.Persist {
al.saveChan = make(chan Item)
go func() {
for v := range al.saveChan {
if err = al.save(v); err != nil {
break
}
}
}()
}
if err != nil {
return nil, err
}
return &al, nil
}
func (al *List) IsPersist() bool {
return al.saveChan != nil
}
func (al *List) Set(vars []byte, query, comment string) error {
if al.saveChan == nil {
return errors.New("allow.list is read-only")
}
if len(query) == 0 {
return errors.New("empty query")
}
var q string
for i := 0; i < len(query); i++ {
c := query[i]
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' {
q = query
break
} else if c == '{' {
q = "query " + query
break
}
}
al.saveChan <- Item{
Comment: comment,
Query: q,
Vars: vars,
}
return nil
}
func (al *List) Load() ([]Item, error) {
var list []Item
b, err := ioutil.ReadFile(al.filepath)
if err != nil {
return list, err
}
if len(b) == 0 {
return list, nil
}
var comment bytes.Buffer
var varBytes []byte
itemMap := make(map[string]struct{})
s, e, c := 0, 0, 0
ty := 0
for {
fq := false
if c == 0 && b[e] == '#' {
s = e
for e < len(b) && b[e] != '\n' {
e++
}
if (e - s) > 2 {
comment.Write(b[(s + 1):(e + 1)])
}
}
if e >= len(b) {
break
}
if matchPrefix(b, e, "query") || matchPrefix(b, e, "mutation") {
if c == 0 {
s = e
}
ty = AL_QUERY
} else if matchPrefix(b, e, "variables") {
if c == 0 {
s = e + len("variables") + 1
}
ty = AL_VARS
} else if b[e] == '{' {
c++
} else if b[e] == '}' {
c--
if c == 0 {
if ty == AL_QUERY {
fq = true
} else if ty == AL_VARS {
varBytes = b[s:(e + 1)]
}
ty = 0
}
}
if fq {
query := string(b[s:(e + 1)])
name := QueryName(query)
key := strings.ToLower(name)
if _, ok := itemMap[key]; !ok {
v := Item{
Name: name,
key: key,
Query: query,
Vars: varBytes,
Comment: comment.String(),
}
list = append(list, v)
comment.Reset()
}
varBytes = nil
}
e++
if e >= len(b) {
break
}
}
return list, nil
}
func (al *List) save(item Item) error {
item.Name = QueryName(item.Query)
item.key = strings.ToLower(item.Name)
if len(item.Name) == 0 {
return nil
}
list, err := al.Load()
if err != nil {
return err
}
index := -1
for i, v := range list {
if strings.EqualFold(v.Name, item.Name) {
index = i
break
}
}
if index != -1 {
if len(list[index].Comment) != 0 {
item.Comment = list[index].Comment
}
list[index] = item
} else {
list = append(list, item)
}
f, err := os.Create(al.filepath)
if err != nil {
return err
}
defer f.Close()
sort.Slice(list, func(i, j int) bool {
return strings.Compare(list[i].key, list[j].key) == -1
})
for _, v := range list {
cmtLines := strings.Split(v.Comment, "\n")
i := 0
for _, c := range cmtLines {
if c = strings.TrimSpace(c); len(c) == 0 {
continue
}
_, err := f.WriteString(fmt.Sprintf("# %s\n", c))
if err != nil {
return err
}
i++
}
if i != 0 {
if _, err := f.WriteString("\n"); err != nil {
return err
}
} else {
if _, err := f.WriteString(fmt.Sprintf("# Query named %s\n\n", v.Name)); err != nil {
return err
}
}
if len(v.Vars) != 0 && !bytes.Equal(v.Vars, []byte("{}")) {
vj, err := json.MarshalIndent(v.Vars, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal vars: %v", err)
}
_, err = f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
if err != nil {
return err
}
}
if v.Query[0] == '{' {
_, err = f.WriteString(fmt.Sprintf("query %s\n\n", v.Query))
} else {
_, err = f.WriteString(fmt.Sprintf("%s\n\n", v.Query))
}
if err != nil {
return err
}
}
return nil
}
func matchPrefix(b []byte, i int, s string) bool {
if (len(b) - i) < len(s) {
return false
}
for n := 0; n < len(s); n++ {
if b[(i+n)] != s[n] {
return false
}
}
return true
}
func QueryName(b string) string {
state, s := 0, 0
for i := 0; i < len(b); i++ {
switch {
case state == 2 && !isValidNameChar(b[i]):
return b[s:i]
case state == 1 && b[i] == '{':
return ""
case state == 1 && isValidNameChar(b[i]):
s = i
state = 2
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
state = 1
}
}
return ""
}
func isValidNameChar(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
}

84
allow/allow_test.go Normal file
View File

@ -0,0 +1,84 @@
package allow
import (
"testing"
)
func TestGQLName1(t *testing.T) {
var q = `
query {
products(
distinct: [price]
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
) { id name } }`
name := QueryName(q)
if len(name) != 0 {
t.Fatal("Name should be empty, not ", name)
}
}
func TestGQLName2(t *testing.T) {
var q = `
query hakuna_matata
{
products(
distinct: [price]
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
) {
id
name
}
}`
name := QueryName(q)
if name != "hakuna_matata" {
t.Fatal("Name should be 'hakuna_matata', not ", name)
}
}
func TestGQLName3(t *testing.T) {
var q = `
mutation means{ users { id } }`
// var v2 = ` { products( limit: 30, order_by: { price: desc }, distinct: [ price ] where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) { id name price user { id email } } } `
name := QueryName(q)
if name != "means" {
t.Fatal("Name should be 'means', not ", name)
}
}
func TestGQLName4(t *testing.T) {
var q = `
query no_worries
users {
id
}
}`
name := QueryName(q)
if name != "no_worries" {
t.Fatal("Name should be 'no_worries', not ", name)
}
}
func TestGQLName5(t *testing.T) {
var q = `
{
users {
id
}
}`
name := QueryName(q)
if len(name) != 0 {
t.Fatal("Name should be empty, not ", name)
}
}

15
allow/fuzz_test.go Normal file
View File

@ -0,0 +1,15 @@
package allow
import "testing"
func TestFuzzCrashers(t *testing.T) {
var crashers = []string{
"query",
"q",
"que",
}
for _, f := range crashers {
_ = QueryName(f)
}
}

View File

@ -32,6 +32,10 @@ reload_on_config_change: true
# Path pointing to where the migrations can be found # Path pointing to where the migrations can be found
migrations_path: ./config/migrations migrations_path: ./config/migrations
# Secret key for general encryption operations like
# encrypting the cursor data
secret_key: supercalifajalistics
# Postgres related environment Variables # Postgres related environment Variables
# SG_DATABASE_HOST # SG_DATABASE_HOST
# SG_DATABASE_PORT # SG_DATABASE_PORT
@ -167,10 +171,13 @@ roles:
block: false block: false
- name: deals - name: deals
query: query:
limit: 3 limit: 3
columns: ["name", "description" ] aggregation: false
- name: purchases
query:
limit: 3
aggregation: false aggregation: false
- name: user - name: user
@ -183,12 +190,10 @@ roles:
query: query:
limit: 50 limit: 50
filters: ["{ user_id: { eq: $user_id } }"] filters: ["{ user_id: { eq: $user_id } }"]
columns: ["id", "name", "description", "search_rank", "search_headline_description" ]
disable_functions: false disable_functions: false
insert: insert:
filters: ["{ user_id: { eq: $user_id } }"] filters: ["{ user_id: { eq: $user_id } }"]
columns: ["id", "name", "description" ]
presets: presets:
- user_id: "$user_id" - user_id: "$user_id"
- created_at: "now" - created_at: "now"

View File

@ -32,6 +32,10 @@ enable_tracing: true
# Path pointing to where the migrations can be found # Path pointing to where the migrations can be found
# migrations_path: migrations # migrations_path: migrations
# Secret key for general encryption operations like
# encrypting the cursor data
# secret_key: supercalifajalistics
# Postgres related environment Variables # Postgres related environment Variables
# SG_DATABASE_HOST # SG_DATABASE_HOST
# SG_DATABASE_PORT # SG_DATABASE_PORT

View File

@ -11,7 +11,7 @@ for (i = 0; i < user_count; i++) {
var pwd = fake.password() var pwd = fake.password()
var data = { var data = {
full_name: fake.name(), full_name: fake.name(),
avatar: fake.image_url(), avatar: fake.avatar_url(200),
phone: fake.phone(), phone: fake.phone(),
email: fake.email(), email: fake.email(),
password: pwd, password: pwd,

80
crypto/encrypt.go Normal file
View File

@ -0,0 +1,80 @@
// cryptopasta - basic cryptography examples
//
// Written in 2015 by George Tankersley <george.tankersley@gmail.com>
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see // <http://creativecommons.org/publicdomain/zero/1.0/>.
// Provides symmetric authenticated encryption using 256-bit AES-GCM with a random nonce.
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
)
// NewEncryptionKey generates a random 256-bit key for Encrypt() and
// Decrypt(). It panics if the source of randomness fails.
func NewEncryptionKey() [32]byte {
key := [32]byte{}
_, err := io.ReadFull(rand.Reader, key[:])
if err != nil {
panic(err)
}
return key
}
// Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of
// the data and provides a check that it hasn't been altered. Output takes the
// form nonce|ciphertext|tag where '|' indicates concatenation.
func Encrypt(plaintext []byte, key *[32]byte) (ciphertext []byte, err error) {
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
// Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of
// the data and provides a check that it hasn't been altered. Expects input
// form nonce|ciphertext|tag where '|' indicates concatenation.
func Decrypt(ciphertext []byte, key *[32]byte) (plaintext []byte, err error) {
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(ciphertext) < gcm.NonceSize() {
return nil, errors.New("malformed ciphertext")
}
return gcm.Open(nil,
ciphertext[:gcm.NonceSize()],
ciphertext[gcm.NonceSize():],
nil,
)
}

View File

@ -137,7 +137,7 @@
</h1> </h1>
<div class="text-2xl md:text-3xl"> <div class="text-2xl md:text-3xl">
<small class="text-sm">Download the Docker compose config for the demo</small> <small class="text-sm">Download the Docker compose config for the demo</small>
<pre>&#8227; curl -L -o demo.yml https://bit.ly/2mq05lW</pre> <pre>&#8227; curl -L -o demo.yml https://bit.ly/2FZS0uw</pre>
<small class="text-sm">Setup the demo database</small> <small class="text-sm">Setup the demo database</small>
<pre>&#8227; docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed</pre> <pre>&#8227; docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed</pre>

View File

@ -4,9 +4,9 @@ sidebar: auto
# Guide to Super Graph # Guide to Super Graph
Super Graph is a micro-service that instantly and without code gives you a high performance and secure GraphQL API. Your GraphQL queries are auto translated into a single fast SQL query. No more writing API code as you develop your web frontend just make the query you need and Super Graph will do the rest. Super Graph is a service that instantly and without code gives you a high performance and secure GraphQL API. Your GraphQL queries are auto translated into a single fast SQL query. No more spending weeks or months writing backend API code. Just make the query you need and Super Graph will do the rest.
Super Graph has a rich feature set like integrating with your existing Ruby on Rails apps, joining your DB with data from remote APIs, Role and Attribute based access control, Supoport for JWT tokens, DB migrations, seeding and a lot more. Super Graph has a rich feature set like integrating with your existing Ruby on Rails apps, joining your DB with data from remote APIs, Role and Attribute based access control, Support for JWT tokens, DB migrations, seeding and a lot more.
## Features ## Features
@ -31,14 +31,14 @@ Super Graph has a rich feature set like integrating with your existing Ruby on R
## Try the demo app ## Try the demo app
```bash ```bash
# download the Docker compose config for the demo # clone the repository
curl -L -o demo.yml https://bit.ly/2mq05lW git clone https://github.com/dosco/super-graph
# setup the demo rails app & database and run it # setup the demo rails app & database and run it
docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed docker-compose run rails_app rake db:create db:migrate db:seed
# run the demo # run the demo
docker-compose -f demo.yml up docker-compose up
# signin to the demo app (user1@demo.com / 123456) # signin to the demo app (user1@demo.com / 123456)
open http://localhost:3000 open http://localhost:3000
@ -47,14 +47,14 @@ open http://localhost:3000
open http://localhost:8080 open http://localhost:8080
``` ```
::: warning DEMO REQUIREMENTS ::: tip DEMO REQUIREMENTS
This demo requires `docker` you can either install it using `brew` or from the This demo requires `docker` you can either install it using `brew` or from the
docker website [https://docs.docker.com/docker-for-mac/install/](https://docs.docker.com/docker-for-mac/install/) docker website [https://docs.docker.com/docker-for-mac/install/](https://docs.docker.com/docker-for-mac/install/)
::: :::
#### Trying out GraphQL #### Trying out GraphQL
We currently fully support queries and mutations. Support for `subscriptions` is work in progress. For example the below GraphQL query would fetch two products that belong to the current user where the price is greater than 10. We fully support queries and mutations. For example the below GraphQL query would fetch two products that belong to the current user where the price is greater than 10.
#### GQL Query #### GQL Query
@ -76,32 +76,6 @@ query {
} }
``` ```
In another example the below GraphQL mutation would insert a product into the database. The first part of the below example is the variable data and the second half is the GraphQL mutation. For mutations data has to always ben passed as a variable.
```json
{
"data": {
"name": "Art of Computer Programming",
"description": "The Art of Computer Programming (TAOCP) is a comprehensive monograph written by computer scientist Donald Knuth",
"price": 30.5
}
}
```
```graphql
mutation {
product(insert: $data) {
id
name
}
}
```
The above GraphQL query returns the JSON result below. It handles all
kinds of complexity without you having to writing a line of code.
For example there is a while greater than `gt` and a limit clause on a child field. And the `avatar` field is renamed to `picture`. The `password` field is blocked and not returned. Finally the relationship between the `users` table and the `products` table is auto discovered and used.
#### JSON Result #### JSON Result
```json ```json
@ -128,19 +102,107 @@ For example there is a while greater than `gt` and a limit clause on a child fie
} }
``` ```
#### Try with an authenticated user ::: tip Testing with a user
In development mode you can use the `X-User-ID: 4` header to set a user id so you don't have to worries about cookies etc. This can be set using the *HTTP Headers* tab at the bottom of the web UI.
:::
In development mode you can use the `X-User-ID: 4` header to set a user id so you don't have to worries about cookies etc. This can be set using the *HTTP Headers* tab at the bottom of the web UI you'll see when you visit the above link. You can also directly run queries from the commandline like below. In another example the below GraphQL mutation would insert a product into the database. The first part of the below example is the variable data and the second half is the GraphQL mutation. For mutations data has to always ben passed as a variable.
#### Querying the GQL endpoint ```json
{
"data": {
"name": "Art of Computer Programming",
"description": "The Art of Computer Programming (TAOCP) is a comprehensive monograph written by computer scientist Donald Knuth",
"price": 30.5
}
}
```
```bash ```graphql
mutation {
product(insert: $data) {
id
name
}
}
```
# fetch the response json directly from the endpoint using user id 5 ## Why Super Graph
curl 'http://localhost:8080/api/v1/graphql' \
-H 'content-type: application/json' \ Let's take a simple example say you want to fetch 5 products priced over 12 dollars along with the photos of the products and the users that owns them. Additionally also fetch the last 10 of your own purchases along with the name and ID of the product you purchased. This is a common type of query to render a view in say an ecommerce app. Lets be honest it's not very exciting write and maintain. Keep in mind the data needed will only continue to grow and change as your app evolves. Developers might find that most ORMs will not be able to do all of this in a single SQL query and will require n+1 queries to fetch all the data and assembly it into the right JSON response.
-H 'X-User-ID: 5' \
--data-binary '{"query":"{ products { name price users { email }}}"}' What if I told you Super Graph will fetch all this data with a single SQL query and without you having to write a single line of code. Also as your app evolves feel free to evolve the query as you like. In our experience Super Graph saves us hundreds or thousands of man hours that we can put towards the more exciting parts of our app.
#### GraphQL Query
```graphql
query {
products(limit 5, where: { price: { gt: 12 } }) {
id
name
description
price
photos {
url
}
user {
id
email
picture : avatar
full_name
}
}
purchases(
limit 10,
order_by: { created_at: desc } ,
where: { user_id: { eq: $user_id } }
) {
id
created_at
product {
id
name
}
}
}
```
#### JSON Result
```json
"data": {
"products": [
{
"id": 1,
"name": "Oaked Arrogant Bastard Ale",
"description": "Coors lite, European Amber Lager, Perle, 1272 - American Ale II, 38 IBU, 6.4%, 9.7°Blg",
"price": 20,
"photos: [{
"url": "https://www.scienceworld.ca/wp-content/uploads/science-world-beer-flavours.jpg"
}],
"user": {
"id": 1,
"email": "user0@demo.com",
"picture": "https://robohash.org/sitaliquamquaerat.png?size=300x300&set=set1",
"full_name": "Mrs. Wilhemina Hilpert"
}
},
...
]
},
"purchases": [
{
"id": 5,
"created_at": "2020-01-24T05:34:39.880599",
"product": {
"id": 45,
"name": "Brooklyn Black",
}
},
...
]
}
``` ```
## Get Started ## Get Started
@ -651,8 +713,6 @@ query {
} }
``` ```
## Mutations
In GraphQL mutations is the operation type for when you need to modify data. Super Graph supports the `insert`, `update`, `upsert` and `delete`. You can also do complex nested inserts and updates. In GraphQL mutations is the operation type for when you need to modify data. Super Graph supports the `insert`, `update`, `upsert` and `delete`. You can also do complex nested inserts and updates.
When using mutations the data must be passed as variables since Super Graphs compiles the query into an prepared statement in the database for maximum speed. Prepared statements are are functions in your code when called they accept arguments and your variables are passed in as those arguments. When using mutations the data must be passed as variables since Super Graphs compiles the query into an prepared statement in the database for maximum speed. Prepared statements are are functions in your code when called they accept arguments and your variables are passed in as those arguments.
@ -836,8 +896,6 @@ mutation {
} }
``` ```
## Nested Mutations
Often you will need to create or update multiple related items at the same time. This can be done using nested mutations. For example you might need to create a product and assign it to a user, or create a user and his products at the same time. You just have to use simple json to define you mutation and Super Graph takes care of the rest. Often you will need to create or update multiple related items at the same time. This can be done using nested mutations. For example you might need to create a product and assign it to a user, or create a user and his products at the same time. You just have to use simple json to define you mutation and Super Graph takes care of the rest.
### Nested Insert ### Nested Insert
@ -906,7 +964,7 @@ mutation {
} }
``` ```
### Nested Updates ### Nested Update
Update a product item first and then assign it to a user Update a product item first and then assign it to a user
@ -966,7 +1024,114 @@ mutation {
} }
``` ```
## Using variables ### Pagination
This is a must have feature of any API. When you want your users to go thought a list page by page or implement some fancy infinite scroll you're going to need pagination. There are two ways to paginate in Super Graph.
Limit-Offset
This is simple enough but also inefficient when working with a large number of total items. Limit, limits the number of items fetched and offset is the point you want to fetch from. The below query will fetch 10 results at a time starting with the 100th item. You will have to keep updating offset (110, 120, 130, etc ) to walk thought the results so make offset a variable.
```graphql
query {
products(limit: 10, offset: 100) {
id
slug
name
}
}
```
#### Cursor
This is a powerful and highly efficient way to paginate though a large number of results. Infact it does not matter how many total results there are this will always be lighting fast. You can use a cursor to walk forward of backward though the results. If you plan to implement infinite scroll this is the option you should choose.
When going this route the results will contain a cursor value this is an encrypted string that you don't have to worry about just pass this back in to the next API call and you'll received the next set of results. The cursor value is encrypted since its contents should only matter to Super Graph and not the client. Also since the primary key is used for this feature it's possible you might not want to leak it's value to clients.
You will need to set this config value to ensure the encrypted cursor data is secure. If not set a random value is used which will change with each deployment breaking older cursor values that clients might be using so best to set it.
```yaml
# Secret key for general encryption operations like
# encrypting the cursor data
secret_key: supercalifajalistics
```
Paginating forward through your results
```json
{
"variables": {
"cursor": "MJoTLbQF4l0GuoDsYmCrpjPeaaIlNpfm4uFU4PQ="
}
}
```
```graphql
query {
products(first: 10, after: $cursor) {
slug
name
}
}
```
Paginating backward through your results
```graphql
query {
products(last: 10, before: $cursor) {
slug
name
}
}
```
```graphql
"data": {
"products": [
{
"slug": "eius-nulla-et-8",
"name" "Pale Ale"
},
{
"slug": "sapiente-ut-alias-12",
"name" "Brown Ale"
}
...
],
"products_cursor": "dJwHassm5+d82rGydH2xQnwNxJ1dcj4/cxkh5Cer"
}
```
Nested tables can also have cursors. Requesting multiple cursors are supported on a single request but when paginating using a cursor only one table is currently supported. To explain this better, you can only use a `before` or `after` argument with a cursor value to paginate a single table in a query.
```graphql
query {
products(last: 10) {
slug
name
customers(last: 5) {
email
full_name
}
}
}
```
Multiple order-by arguments are supported. Super Graph is smart enough to allow cursor based pagination when you also need complex sort order like below.
```graphql
query {
products(
last: 10
before: $cursor
order_by: [ price: desc, total_customers: asc ]) {
slug
name
}
}
```
## Using Variables
Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The build-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The build-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner
and better client side code. and better client side code.
@ -988,6 +1153,104 @@ fetch('http://localhost:8080/api/v1/graphql', {
.then(res => console.log(res.data)); .then(res => console.log(res.data));
``` ```
## GraphQL with React
This is a quick simple example using `graphql.js` [https://github.com/f/graphql.js/](https://github.com/f/graphql.js/)
```js
import React, { useState, useEffect } from 'react'
import graphql from 'graphql.js'
// Create a GraphQL client pointing to Super Graph
var graph = graphql("http://localhost:3000/api/v1/graphql", { asJSON: true })
const App = () => {
const [user, setUser] = useState(null)
useEffect(() => {
async function action() {
// Use the GraphQL client to execute a graphQL query
// The second argument to the client are the variables you need to pass
const result = await graph(`{ user { id first_name last_name picture_url } }`)()
setUser(result)
}
action()
}, []);
return (
<div className="App">
<h1>{ JSON.stringify(user) }</h1>
</div>
);
}
```
export default App;
## Advanced Columns
The ablity to have `JSON/JSONB` and `Array` columns is often considered in the top most useful features of Postgres. There are many cases where using an array or a json column saves space and reduces complexity in your app. The only issue with these columns is the really that your SQL queries can get harder to write and maintain.
Super Graph steps in here to help you by supporting these columns right out of the box. It allows you to work with these columns just like you would with tables. Joining data against or modifying array columns using the `connect` or `disconnect` keywords in mutations is fully supported. Another very useful feature is the ability to treat `json` or `binary json (jsonb)` columns as seperate tables, even using them in nested queries joining against related tables. To replicate these features on your own will take a lot of complex SQL. Using Super Graph means you don't have to deal with any of this it just works.
### Array Columns
Configure a relationship between an array column `tag_ids` which contains integer id's for tags and the column `id` in the table `tags`.
```yaml
tables:
- name: posts
columns:
- name: tag_ids
related_to: tags.id
```
```graphql
query {
posts {
title
tags {
name
image
}
}
}
```
### JSON Column
Configure a JSON column called `tag_count` in the table `products` into a seperate table. This JSON column contains a json array of objects each with a tag id and a count of the number of times the tag was used. As a seperate table you can nest it into your GraphQL query and treat it like table using any of the standard features like `order_by`, `limit`, `where clauses`, etc.
The configuration below tells Super Graph to create a synthetic table called `tag_count` using the column `tag_count` from the `products` table. And that this new table has two columns `tag_id` and `count` of the listed types and with the defined relationships.
```yaml
tables:
- name: tag_count
table: products
columns:
- name: tag_id
type: bigint
related_to: tags.id
- name: count
type: int
```
```graphql
query {
products {
name
tag_counts {
count
tag {
name
}
}
}
}
```
## Full text search ## Full text search
Every app these days needs search. Enought his often means reaching for something heavy like Solr. While this will work why add complexity to your infrastructure when Postgres has really great Every app these days needs search. Enought his often means reaching for something heavy like Solr. While this will work why add complexity to your infrastructure when Postgres has really great
@ -1073,45 +1336,45 @@ class AddSearchColumn < ActiveRecord::Migration[5.1]
end end
``` ```
## GraphQL with React ## API Security
This is a quick simple example using `graphql.js` [https://github.com/f/graphql.js/](https://github.com/f/graphql.js/) One of the the most common questions I get asked is what happens if a user out on the internet sends queries
that we don't want run. For example how do we stop him from fetching all users or the emails of users. Our answer to this is that it is not an issue as this cannot happen, let me explain.
```js Super Graph runs in one of two modes `development` or `production`, this is controlled via the config value `production: false` when it's false it's running in development mode and when true, production. In development mode all the **named** queries (including mutations) are saved to the allow list `./config/allow.list`. While in production mode when Super Graph starts only the queries from this allow list file are registered with the database as [prepared statements](https://stackoverflow.com/questions/8263371/how-can-prepared-statements-protect-from-sql-injection-attacks).
import React, { useState, useEffect } from 'react'
import graphql from 'graphql.js'
// Create a GraphQL client pointing to Super Graph Prepared statements are designed by databases to be fast and secure. They protect against all kinds of sql injection attacks and since they are pre-processed and pre-planned they are much faster to run then raw sql queries. Also there's no GraphQL to SQL compiling happening in production mode which makes your queries lighting fast as they are directly sent to the database with almost no overhead.
var graph = graphql("http://localhost:3000/api/v1/graphql", { asJSON: true })
const App = () => { In short in production only queries listed in the allow list file `./config/allow.list` can be used, all other queries will be blocked.
const [user, setUser] = useState(null)
useEffect(() => { ::: tip How to think about the allow list?
async function action() { The allow list file is essentially a list of all your exposed API calls and the data that passes within them. It's very easy to build tooling to do things like parsing this file within your tests to ensure fields like `credit_card_no` are not accidently leaked. It's a great way to build compliance tooling and ensure your user data is always safe.
// Use the GraphQL client to execute a graphQL query :::
// The second argument to the client are the variables you need to pass
const result = await graph(`{ user { id first_name last_name picture_url } }`)() This is an example of a named query, `getUserWithProducts` is the name you've given to this query it can be anything you like but should be unique across all you're queries. Only named queries are saved in the allow list in development mode.
setUser(result)
```graphql
query getUserWithProducts {
users {
id
name
products {
id
name
price
}
} }
action()
}, []);
return (
<div className="App">
<h1>{ JSON.stringify(user) }</h1>
</div>
);
} }
``` ```
export default App;
## Authentication ## Authentication
You can only have one type of auth enabled. You can either pick Rails or JWT. You can only have one type of auth enabled either Rails or JWT.
### Rails Auth (Devise / Warden) ### Ruby on Rails
Almost all Rails apps use Devise or Warden for authentication. Once the user is Almost all Rails apps use Devise or Warden for authentication. Once the user is
authenticated a session is created with the users ID. The session can either be authenticated a session is created with the users ID. The session can either be
@ -1163,7 +1426,7 @@ auth:
max_active: 12000 max_active: 12000
``` ```
### JWT Token Auth ### JWT Tokens
```yaml ```yaml
auth: auth:
@ -1177,14 +1440,67 @@ auth:
public_key_type: ecdsa #rsa public_key_type: ecdsa #rsa
``` ```
For JWT tokens we currently support tokens from a provider like Auth0 For JWT tokens we currently support tokens from a provider like Auth0 or if you have a custom solution then we look for the `user_id` in the `subject` claim of of the `id token`. If you pick Auth0 then we derive two variables from the token `user_id` and `user_id_provider` for to use in your filters.
or if you have a custom solution then we look for the `user_id` in the
`subject` claim of of the `id token`. If you pick Auth0 then we derive two variables from the token `user_id` and `user_id_provider` for to use in your filters.
We can get the JWT token either from the `authorization` header where we expect it to be a `bearer` token or if `cookie` is specified then we look there. We can get the JWT token either from the `authorization` header where we expect it to be a `bearer` token or if `cookie` is specified then we look there.
For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file. For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
### HTTP Headers
```yaml
header:
name: X-AppEngine-QueueName
exists: true
#value: default
```
Header auth is usually the best option to authenticate requests to the action endpoints. For example you
might want to use an action to refresh a materalized view every hour and only want a cron service like the Google AppEngine Cron service to make that request in this case a config similar to the one above will do.
The `exists: true` parameter ensures that only the existance of the header is checked not its value. The `value` parameter lets you confirm that the value matches the one assgined to the parameter. This helps in the case you are using a shared secret to protect the endpoint.
### Named Auth
```yaml
# You can add additional named auths to use with actions
# In this example actions using this auth can only be
# called from the Google Appengine Cron service that
# sets a special header to all it's requests
auths:
- name: from_taskqueue
type: header
header:
name: X-Appengine-Cron
exists: true
```
In addition to the default auth configuration you can create additional named auth configurations to be used
with features like `actions`. For example while your main GraphQL endpoint uses JWT for authentication you may want to use a header value to ensure your actions can only be called by clients having access to a shared secret
or security header.
## Actions
Actions is a very useful feature that is currently work in progress. For now the best use case for actions is to
refresh database tables like materialized views or call a database procedure to refresh a cache table, etc. An action creates an http endpoint that anyone can call to have the SQL query executed. The below example will create an endpoint `/api/v1/actions/refresh_leaderboard_users` any request send to that endpoint will cause the sql query to be executed. the `auth_name` points to a named auth that should be used to secure this endpoint. In future we have big plans to allow your own custom code to run using actions.
```yaml
actions:
- name: refresh_leaderboard_users
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
auth_name: from_taskqueue
```
#### Using CURL to test a query
```bash
# fetch the response json directly from the endpoint using user id 5
curl 'http://localhost:8080/api/v1/graphql' \
-H 'content-type: application/json' \
-H 'X-User-ID: 5' \
--data-binary '{"query":"{ products { name price users { email }}}"}'
```
## Access Control ## Access Control
It's common for APIs to control what information they return or insert based on the role of the user. In Super Graph we have two primary roles `user` and `anon` the first for users where a `user_id` is available the latter for users where it's not. It's common for APIs to control what information they return or insert based on the role of the user. In Super Graph we have two primary roles `user` and `anon` the first for users where a `user_id` is available the latter for users where it's not.
@ -1197,7 +1513,6 @@ The `user` role can be divided up into further roles based on attributes in the
Super Graph allows you to create roles dynamically using a `roles_query` and ` match` config values. Super Graph allows you to create roles dynamically using a `roles_query` and ` match` config values.
### Configure RBAC ### Configure RBAC
```yaml ```yaml
@ -1334,7 +1649,7 @@ tables:
``` ```
## Configuration files ## Configuration
Configuration files can either be in YAML or JSON their names are derived from the `GO_ENV` variable, for example `GO_ENV=prod` will cause the `prod.yaml` config file to be used. or `GO_ENV=dev` will use the `dev.yaml`. A path to look for the config files in can be specified using the `-path <folder>` command line argument. Configuration files can either be in YAML or JSON their names are derived from the `GO_ENV` variable, for example `GO_ENV=prod` will cause the `prod.yaml` config file to be used. or `GO_ENV=dev` will use the `dev.yaml`. A path to look for the config files in can be specified using the `-path <folder>` command line argument.
@ -1430,6 +1745,22 @@ auth:
# public_key_file: /secrets/public_key.pem # public_key_file: /secrets/public_key.pem
# public_key_type: ecdsa #rsa # public_key_type: ecdsa #rsa
# header:
# name: dnt
# exists: true
# value: localhost:8080
# You can add additional named auths to use with actions
# In this example actions using this auth can only be
# called from the Google Appengine Cron service that
# sets a special header to all it's requests
auths:
- name: from_taskqueue
type: header
header:
name: X-Appengine-Cron
exists: true
database: database:
type: postgres type: postgres
host: db host: db
@ -1460,6 +1791,17 @@ database:
- encrypted - encrypted
- token - token
# Create custom actions with their own api endpoints
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
# A request to this url will execute the configured SQL query
# which in this case refreshes a materialized view in the database.
# The auth_name is from one of the configured auths
actions:
- name: refresh_leaderboard_users
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
auth_name: from_taskqueue
tables: tables:
- name: customers - name: customers
remotes: remotes:
@ -1560,6 +1902,71 @@ SG_AUTH_RAILS_REDIS_PASSWORD
SG_AUTH_JWT_PUBLIC_KEY_FILE SG_AUTH_JWT_PUBLIC_KEY_FILE
``` ```
## YugabyteDB
Yugabyte is an open-source, geo-distrubuted cloud-native relational DB that scales horizontally. Super Graph works with Yugabyte right out of the box. If you think you're data needs will outgrow Postgres and you don't really want to deal with sharding then Yugabyte is the way to go. Just point Super Graph to your Yugabyte DB and everything will just work including running migrations, seeding, querying, mutations, etc.
To use Yugabyte in your local development flow just uncomment the following lines in the `docker-compose.yml` file that is part of your Super Graph app. Also remember to comment out the originl postgres `db` config.
```yaml
# Postgres DB
# db:
# image: postgres:latest
# ports:
# - "5432:5432"
#Standard config to run a single node of Yugabyte
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
# Environment variables to point Super Graph to Yugabyte
# This is required since it uses a different user and port number
yourapp_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:
- "8080:8080"
depends_on:
- db
```
## Developing Super Graph ## Developing Super Graph
If you want to build and run Super Graph from code then the below commands will build the web ui and launch Super Graph in developer mode with a watcher to rebuild on code changes. And the demo rails app is also launched to make it essier to test changes. If you want to build and run Super Graph from code then the below commands will build the web ui and launch Super Graph in developer mode with a watcher to rebuild on code changes. And the demo rails app is also launched to make it essier to test changes.

1
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dlclark/regexp2 v1.2.0 // indirect github.com/dlclark/regexp2 v1.2.0 // indirect
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c // indirect
github.com/fsnotify/fsnotify v1.4.7 github.com/fsnotify/fsnotify v1.4.7
github.com/garyburd/redigo v1.6.0 github.com/garyburd/redigo v1.6.0
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect

2
go.sum
View File

@ -54,6 +54,8 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw= github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw=
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c h1:/bXaeEuNG6V0HeyEGw11DYLW5BGsOPlcVRIXbHNUWSo=
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=

9
jsn/bench.0 Normal file
View File

@ -0,0 +1,9 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/jsn
BenchmarkGet-8 13310 88437 ns/op 3328 B/op 2 allocs/op
BenchmarkFilter-8 182232 6922 ns/op 448 B/op 1 allocs/op
BenchmarkStrip-8 162709 6560 ns/op 224 B/op 1 allocs/op
BenchmarkReplace-8 85846 13996 ns/op 416 B/op 1 allocs/op
PASS
ok github.com/dosco/super-graph/jsn 5.913s

View File

@ -109,7 +109,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
case state == expectValue && b[i] == 'n': case state == expectValue && b[i] == 'n':
state = expectNull state = expectNull
case state == expectNull && b[i] == 'l': case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
e = i e = i
} }

View File

@ -117,8 +117,9 @@ func Get(b []byte, keys [][]byte) []Field {
case state == expectValue && b[i] == 'n': case state == expectValue && b[i] == 'n':
state = expectNull state = expectNull
s = i
case state == expectNull && b[i] == 'l': case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
e = i e = i
} }
@ -130,6 +131,20 @@ func Get(b []byte, keys [][]byte) []Field {
n++ n++
} }
if state == expectListClose {
loop:
for j := i + 1; j < len(b); j++ {
switch b[j] {
case ' ', '\t', '\n':
continue
case '{':
break loop
}
i = e
break loop
}
}
state = expectKey state = expectKey
e = 0 e = 0
} }

View File

@ -9,11 +9,11 @@ var (
input1 = ` input1 = `
{ {
"data": { "data": {
"test": { "__twitter_id": "ABCD" }, "test_1a": { "__twitter_id": "ABCD" },
"users": [ "users": [
{ {
"id": 1, "id": 1,
"full_name": "'Sidney Stroman'", "full_name": "'Sidney St[1]roman'",
"email": "user0@demo.com", "email": "user0@demo.com",
"__twitter_id": "2048666903444506956", "__twitter_id": "2048666903444506956",
"embed": { "embed": {
@ -108,7 +108,7 @@ var (
input2 = ` input2 = `
[{ [{
"id": 1, "id": 1,
"full_name": "Sidney Stroman", "full_name": "Sidney St[1]roman",
"email": "user0@demo.com", "email": "user0@demo.com",
"__twitter_id": "2048666903444506956", "__twitter_id": "2048666903444506956",
"something": null, "something": null,
@ -130,7 +130,7 @@ var (
input3 = ` input3 = `
{ {
"data": { "data": {
"test": { "__twitter_id": "ABCD" }, "test_1a": { "__twitter_id": "ABCD" },
"users": [{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}] "users": [{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]
} }
}` }`
@ -138,7 +138,7 @@ var (
input4 = ` input4 = `
{ "users" : [{ { "users" : [{
"id": 1, "id": 1,
"full_name": "Sidney Stroman", "full_name": "Sidney St[1]roman",
"email": "user0@demo.com", "email": "user0@demo.com",
"__twitter_id": "2048666903444506956", "__twitter_id": "2048666903444506956",
"embed": { "embed": {
@ -155,24 +155,29 @@ var (
"email": "user1@demo.com", "email": "user1@demo.com",
"__twitter_id": [{ "name": "hello" }, { "name": "world"}] "__twitter_id": [{ "name": "hello" }, { "name": "world"}]
}] }` }] }`
input5 = `
{"data":{"title":"In September 2018, Slovak police stated that Kuciak was murdered because of his investigative work, and that the murder had been ordered.[9][10] They arrested eight suspects,[11] charging three of them with first-degree murder.[11]","topics":["cpp"]},"a":["1111"]},"thread_slug":"in-september-2018-slovak-police-stated-that-kuciak-7929",}`
input6 = `
{"users" : [{"id" : 1, "email" : "vicram@gmail.com", "slug" : "vikram-rangnekar", "threads" : [], "threads_cursor" : null}, {"id" : 3, "email" : "marareilly@lang.name", "slug" : "raymundo-corwin", "threads" : [{"id" : 9, "title" : "Et alias et aut porro praesentium nam in voluptatem reiciendis quisquam perspiciatis inventore eos quia et et enim qui amet."}, {"id" : 25, "title" : "Ipsam quam nemo culpa tempore amet optio sit sed eligendi autem consequatur quaerat rem velit quibusdam quibusdam optio a voluptatem."}], "threads_cursor" : 25}], "users_cursor" : 3}`
) )
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
values := Get([]byte(input1), [][]byte{ values := Get([]byte(input1), [][]byte{
[]byte("test_1a"),
[]byte("__twitter_id"), []byte("__twitter_id"),
[]byte("work_email"), []byte("work_email"),
}) })
expected := []Field{ expected := []Field{
{[]byte("test_1a"), []byte(`{ "__twitter_id": "ABCD" }`)},
{[]byte("__twitter_id"), []byte(`"ABCD"`)}, {[]byte("__twitter_id"), []byte(`"ABCD"`)},
{[]byte("__twitter_id"), []byte(`"2048666903444506956"`)}, {[]byte("__twitter_id"), []byte(`"2048666903444506956"`)},
{[]byte("__twitter_id"), []byte(`"ABC123"`)}, {[]byte("__twitter_id"), []byte(`"ABC123"`)},
{[]byte("__twitter_id"), []byte(`"more123"`)}, {[]byte("__twitter_id"), []byte(`"more123"`)},
{[]byte("__twitter_id"), {[]byte("__twitter_id"), []byte(`[{ "name": "hello" }, { "name": "world"}]`)},
[]byte(`[{ "name": "hello" }, { "name": "world"}]`)}, {[]byte("__twitter_id"), []byte(`{ "name": "\"hellos\"", "address": { "work": "1 infinity loop" } }`)},
{[]byte("__twitter_id"),
[]byte(`{ "name": "\"hellos\"", "address": { "work": "1 infinity loop" } }`),
},
{[]byte("__twitter_id"), []byte(`1234567890`)}, {[]byte("__twitter_id"), []byte(`1234567890`)},
{[]byte("__twitter_id"), []byte(`1.23E`)}, {[]byte("__twitter_id"), []byte(`1.23E`)},
{[]byte("__twitter_id"), []byte(`true`)}, {[]byte("__twitter_id"), []byte(`true`)},
@ -201,6 +206,56 @@ func TestGet(t *testing.T) {
} }
} }
func TestGet1(t *testing.T) {
values := Get([]byte(input5), [][]byte{
[]byte("thread_slug"),
})
expected := []Field{
{[]byte("thread_slug"), []byte(`"in-september-2018-slovak-police-stated-that-kuciak-7929"`)},
}
if len(values) != len(expected) {
t.Fatal("len(values) != len(expected)")
}
for i := range expected {
if !bytes.Equal(values[i].Key, expected[i].Key) {
t.Error(string(values[i].Key), " != ", string(expected[i].Key))
}
if !bytes.Equal(values[i].Value, expected[i].Value) {
t.Error(string(values[i].Value), " != ", string(expected[i].Value))
}
}
}
func TestGet2(t *testing.T) {
values := Get([]byte(input6), [][]byte{
[]byte("users_cursor"), []byte("threads_cursor"),
})
expected := []Field{
{[]byte("threads_cursor"), []byte(`null`)},
{[]byte("threads_cursor"), []byte(`25`)},
{[]byte("users_cursor"), []byte(`3`)},
}
if len(values) != len(expected) {
t.Fatal("len(values) != len(expected)")
}
for i := range expected {
if !bytes.Equal(values[i].Key, expected[i].Key) {
t.Error(string(values[i].Key), " != ", string(expected[i].Key))
}
if !bytes.Equal(values[i].Value, expected[i].Value) {
t.Error(string(values[i].Value), " != ", string(expected[i].Value))
}
}
}
func TestValue(t *testing.T) { func TestValue(t *testing.T) {
v1 := []byte("12345") v1 := []byte("12345")
if !bytes.Equal(Value(v1), v1) { if !bytes.Equal(Value(v1), v1) {
@ -230,7 +285,7 @@ func TestFilter1(t *testing.T) {
t.Error(err) t.Error(err)
} }
expected := `[{"id": 1,"full_name": "Sidney Stroman","embed": {"id": 8,"full_name": "Caroll Orn Sr.","email": "joannarau@hegmann.io","__twitter_id": "ABC123"}},{"id": 2,"full_name": "Jerry Dickinson"}]` expected := `[{"id": 1,"full_name": "Sidney St[1]roman","embed": {"id": 8,"full_name": "Caroll Orn Sr.","email": "joannarau@hegmann.io","__twitter_id": "ABC123"}},{"id": 2,"full_name": "Jerry Dickinson"}]`
if b.String() != expected { if b.String() != expected {
t.Error("Does not match expected json") t.Error("Does not match expected json")
@ -306,7 +361,7 @@ func TestReplace(t *testing.T) {
expected := `{ "users" : [{ expected := `{ "users" : [{
"id": 1, "id": 1,
"full_name": "Sidney Stroman", "full_name": "Sidney St[1]roman",
"email": "user0@demo.com", "email": "user0@demo.com",
"__twitter_id": "2048666903444506956", "__twitter_id": "2048666903444506956",
"embed": { "embed": {
@ -338,7 +393,7 @@ func TestReplace(t *testing.T) {
func TestReplaceEmpty(t *testing.T) { func TestReplaceEmpty(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
json := `{ "users" : [{"id":1,"full_name":"Sidney Stroman","email":"user0@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":2,"full_name":"Jerry Dickinson","email":"user1@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":3,"full_name":"Kenna Cassin","email":"user2@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":4,"full_name":"Mr. Pat Parisian","email":"rodney@kautzer.biz","__users_twitter_id":"2048666903444506956"}, {"id":5,"full_name":"Bette Ebert","email":"janeenrath@goyette.com","__users_twitter_id":"2048666903444506956"}, {"id":6,"full_name":"Everett Kiehn","email":"michael@bartoletti.com","__users_twitter_id":"2048666903444506956"}, {"id":7,"full_name":"Katrina Cronin","email":"loretaklocko@framivolkman.org","__users_twitter_id":"2048666903444506956"}, {"id":8,"full_name":"Caroll Orn Sr.","email":"joannarau@hegmann.io","__users_twitter_id":"2048666903444506956"}, {"id":9,"full_name":"Gwendolyn Ziemann","email":"renaytoy@rutherford.co","__users_twitter_id":"2048666903444506956"}, {"id":10,"full_name":"Mrs. Rosann Fritsch","email":"holliemosciski@thiel.org","__users_twitter_id":"2048666903444506956"}, {"id":11,"full_name":"Arden Koss","email":"cristobalankunding@howewelch.org","__users_twitter_id":"2048666903444506956"}, {"id":12,"full_name":"Brenton Bauch PhD","email":"renee@miller.co","__users_twitter_id":"2048666903444506956"}, {"id":13,"full_name":"Daine Gleichner","email":"andrea@nienow.co","__users_twitter_id":"2048666903444506956"}] }` json := `{ "users" : [{"id":1,"full_name":"Sidney St[1]roman","email":"user0@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":2,"full_name":"Jerry Dickinson","email":"user1@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":3,"full_name":"Kenna Cassin","email":"user2@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":4,"full_name":"Mr. Pat Parisian","email":"rodney@kautzer.biz","__users_twitter_id":"2048666903444506956"}, {"id":5,"full_name":"Bette Ebert","email":"janeenrath@goyette.com","__users_twitter_id":"2048666903444506956"}, {"id":6,"full_name":"Everett Kiehn","email":"michael@bartoletti.com","__users_twitter_id":"2048666903444506956"}, {"id":7,"full_name":"Katrina Cronin","email":"loretaklocko@framivolkman.org","__users_twitter_id":"2048666903444506956"}, {"id":8,"full_name":"Caroll Orn Sr.","email":"joannarau@hegmann.io","__users_twitter_id":"2048666903444506956"}, {"id":9,"full_name":"Gwendolyn Ziemann","email":"renaytoy@rutherford.co","__users_twitter_id":"2048666903444506956"}, {"id":10,"full_name":"Mrs. Rosann Fritsch","email":"holliemosciski@thiel.org","__users_twitter_id":"2048666903444506956"}, {"id":11,"full_name":"Arden Koss","email":"cristobalankunding@howewelch.org","__users_twitter_id":"2048666903444506956"}, {"id":12,"full_name":"Brenton Bauch PhD","email":"renee@miller.co","__users_twitter_id":"2048666903444506956"}, {"id":13,"full_name":"Daine Gleichner","email":"andrea@nienow.co","__users_twitter_id":"2048666903444506956"}] }`
err := Replace(&buf, []byte(json), []Field{}, []Field{}) err := Replace(&buf, []byte(json), []Field{}, []Field{})
if err != nil { if err != nil {
@ -395,7 +450,7 @@ func TestKeys3(t *testing.T) {
json := `{ json := `{
"insert": { "insert": {
"created_at": "now", "created_at": "now",
"test": { "type1": "a", "type2": "b" }, "test_1a": { "type1": "a", "type2": "b" },
"name": "Hello", "name": "Hello",
"updated_at": "now", "updated_at": "now",
"description": "World" "description": "World"
@ -406,7 +461,7 @@ func TestKeys3(t *testing.T) {
fields := Keys([]byte(json)) fields := Keys([]byte(json))
exp := []string{ exp := []string{
"insert", "created_at", "test", "type1", "type2", "name", "updated_at", "description", "insert", "created_at", "test_1a", "type1", "type2", "name", "updated_at", "description",
"user", "user",
} }

View File

@ -101,8 +101,9 @@ func Keys(b []byte) [][]byte {
case state == expectValue && b[i] == 'n': case state == expectValue && b[i] == 'n':
state = expectNull state = expectNull
s = i
case state == expectNull && b[i] == 'l': case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
e = i e = i
} }
@ -111,6 +112,19 @@ func Keys(b []byte) [][]byte {
res = append(res, k) res = append(res, k)
} }
if state == expectListClose {
loop:
for j := i + 1; j < len(b); j++ {
switch b[j] {
case ' ', '\t', '\n':
continue
case '{':
break loop
}
i = e
break loop
}
}
state = expectKey state = expectKey
k = nil k = nil
e = 0 e = 0

View File

@ -104,8 +104,9 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
case state == expectValue && b[i] == 'n': case state == expectValue && b[i] == 'n':
state = expectNull state = expectNull
s = i
case state == expectNull && b[i] == 'l': case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
e = i e = i
} }

View File

@ -82,8 +82,9 @@ func Strip(b []byte, path [][]byte) []byte {
case state == expectValue && b[i] == 'n': case state == expectValue && b[i] == 'n':
state = expectNull state = expectNull
s = i
case state == expectNull && b[i] == 'l': case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
e = i e = i
} }

View File

@ -5,5 +5,5 @@ import (
) )
func main() { func main() {
serv.Init() serv.Cmd()
} }

View File

@ -257,7 +257,7 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
ctx := context.Background() ctx := context.Background()
// Lock to ensure multiple migrations cannot occur simultaneously // Lock to ensure multiple migrations cannot occur simultaneously
lockNum := int64(9628173550095224) // arbitrary random number lockNum := int64(9628173550095224) // arbitrary random number
if _, lockErr := m.conn.Exec(ctx, "select pg_advisory_lock($1)", lockNum); lockErr != nil { if _, lockErr := m.conn.Exec(ctx, "select pg_try_advisory_lock($1)", lockNum); lockErr != nil {
return lockErr return lockErr
} }
defer func() { defer func() {
@ -331,9 +331,9 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
} }
// Reset all database connection settings. Important to do before updating version as search_path may have been changed. // Reset all database connection settings. Important to do before updating version as search_path may have been changed.
if _, err := tx.Exec(ctx, "reset all"); err != nil { // if _, err := tx.Exec(ctx, "reset all"); err != nil {
return err // return err
} // }
// Add one to the version // Add one to the version
_, err = tx.Exec(ctx, "update "+m.versionTable+" set version=$1", sequence) _, err = tx.Exec(ctx, "update "+m.versionTable+" set version=$1", sequence)

198
psql/columns.go Normal file
View File

@ -0,0 +1,198 @@
//nolint:errcheck
package psql
import (
"errors"
"io"
"strings"
"github.com/dosco/super-graph/qcode"
)
func (c *compilerContext) renderBaseColumns(
sel *qcode.Select,
ti *DBTableInfo,
childCols []*qcode.Column,
skipped uint32) ([]int, bool, error) {
var realColsRendered []int
colcount := (len(sel.Cols) + len(sel.OrderBy) + 1)
colmap := make(map[string]struct{}, colcount)
isSearch := sel.Args["search"] != nil
isCursorPaged := sel.Paging.Type != qcode.PtOffset
isAgg := false
i := 0
for n, col := range sel.Cols {
cn := col.Name
colmap[cn] = struct{}{}
_, isRealCol := ti.ColMap[cn]
if isRealCol {
c.renderComma(i)
realColsRendered = append(realColsRendered, n)
colWithTable(c.w, ti.Name, cn)
i++
continue
}
if isSearch && !isRealCol {
switch {
case cn == "search_rank":
if err := c.renderColumnSearchRank(sel, ti, col, i); err != nil {
return nil, false, err
}
i++
case strings.HasPrefix(cn, "search_headline_"):
if err := c.renderColumnSearchHeadline(sel, ti, col, i); err != nil {
return nil, false, err
}
i++
}
} else {
if err := c.renderColumnFunction(sel, ti, col, i); err != nil {
return nil, false, err
}
isAgg = true
i++
}
}
if isCursorPaged {
if _, ok := colmap[ti.PrimaryCol.Key]; !ok {
colmap[ti.PrimaryCol.Key] = struct{}{}
c.renderComma(i)
colWithTable(c.w, ti.Name, ti.PrimaryCol.Name)
}
i++
}
for _, ob := range sel.OrderBy {
if _, ok := colmap[ob.Col]; ok {
continue
}
colmap[ob.Col] = struct{}{}
c.renderComma(i)
colWithTable(c.w, ti.Name, ob.Col)
i++
}
for _, col := range childCols {
if _, ok := colmap[col.Name]; ok {
continue
}
c.renderComma(i)
colWithTable(c.w, col.Table, col.Name)
i++
}
return realColsRendered, isAgg, nil
}
func (c *compilerContext) renderColumnSearchRank(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
if isColumnBlocked(sel, col.Name) {
return nil
}
if ti.TSVCol == nil {
return errors.New("no ts_vector column found")
}
cn := ti.TSVCol.Name
arg := sel.Args["search"]
c.renderComma(columnsRendered)
//fmt.Fprintf(w, `ts_rank("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
//c.sel.Name, cn, arg.Val, col.Name)
io.WriteString(c.w, `ts_rank(`)
colWithTable(c.w, ti.Name, cn)
if c.schema.ver >= 110000 {
io.WriteString(c.w, `, websearch_to_tsquery('{{`)
} else {
io.WriteString(c.w, `, to_tsquery('{{`)
}
io.WriteString(c.w, arg.Val)
io.WriteString(c.w, `}}'))`)
alias(c.w, col.Name)
return nil
}
func (c *compilerContext) renderColumnSearchHeadline(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
cn := col.Name[16:]
if isColumnBlocked(sel, cn) {
return nil
}
arg := sel.Args["search"]
c.renderComma(columnsRendered)
//fmt.Fprintf(w, `ts_headline("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
//c.sel.Name, cn, arg.Val, col.Name)
io.WriteString(c.w, `ts_headline(`)
colWithTable(c.w, ti.Name, cn)
if c.schema.ver >= 110000 {
io.WriteString(c.w, `, websearch_to_tsquery('{{`)
} else {
io.WriteString(c.w, `, to_tsquery('{{`)
}
io.WriteString(c.w, arg.Val)
io.WriteString(c.w, `}}'))`)
alias(c.w, col.Name)
return nil
}
func (c *compilerContext) renderColumnFunction(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
pl := funcPrefixLen(col.Name)
// if pl == 0 {
// //fmt.Fprintf(w, `'%s not defined' AS %s`, cn, col.Name)
// io.WriteString(c.w, `'`)
// io.WriteString(c.w, col.Name)
// io.WriteString(c.w, ` not defined'`)
// alias(c.w, col.Name)
// }
if pl == 0 || !sel.Functions {
return nil
}
cn := col.Name[pl:]
if isColumnBlocked(sel, cn) {
return nil
}
fn := cn[0 : pl-1]
c.renderComma(columnsRendered)
//fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Name, cn, col.Name)
io.WriteString(c.w, fn)
io.WriteString(c.w, `(`)
colWithTable(c.w, ti.Name, cn)
io.WriteString(c.w, `)`)
alias(c.w, col.Name)
return nil
}
func (c *compilerContext) renderComma(columnsRendered int) {
if columnsRendered != 0 {
io.WriteString(c.w, `, `)
}
}
func isColumnBlocked(sel *qcode.Select, name string) bool {
if len(sel.Allowed) != 0 {
if _, ok := sel.Allowed[name]; !ok {
return true
}
}
return false
}

54
psql/fuzz.go Normal file
View File

@ -0,0 +1,54 @@
// +build gofuzz
package psql
import (
"encoding/json"
"github.com/dosco/super-graph/qcode"
)
var (
qcompileTest, _ = qcode.NewCompiler(qcode.Config{})
schema = getTestSchema()
vars = NewVariables(map[string]string{
"admin_account_id": "5",
})
pcompileTest = NewCompiler(Config{
Schema: schema,
Vars: vars,
})
)
// FuzzerEntrypoint for Fuzzbuzz
func Fuzz(data []byte) int {
gql := `mutation {
product(insert: $data) {
id
name
user {
id
full_name
email
}
}
}`
qc, err := qcompileTest.Compile([]byte(gql), "user")
if err != nil {
panic("qcompile can't fail")
}
vars := map[string]json.RawMessage{
"data": json.RawMessage(data),
}
_, _, err = pcompileTest.CompileEx(qc, vars)
if err != nil {
return 0
}
return 1
}

View File

@ -15,7 +15,10 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer,
insert, ok := vars[qc.ActionVar] insert, ok := vars[qc.ActionVar]
if !ok { if !ok {
return 0, fmt.Errorf("Variable '%s' not !defined", qc.ActionVar) return 0, fmt.Errorf("variable '%s' not defined", qc.ActionVar)
}
if len(insert) == 0 {
return 0, fmt.Errorf("variable '%s' is empty", qc.ActionVar)
} }
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`) io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
@ -147,7 +150,14 @@ func renderNestedInsertRelColumns(w io.Writer, item kvitem, values bool) error {
io.WriteString(w, `, `) io.WriteString(w, `, `)
} }
if values { if values {
if v._ctype > 0 {
io.WriteString(w, `"_x_`)
io.WriteString(w, v.relCP.Left.Table)
io.WriteString(w, `".`)
quoted(w, v.relCP.Left.Col)
} else {
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col) colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col)
}
} else { } else {
quoted(w, v.relCP.Right.Col) quoted(w, v.relCP.Right.Col)
} }
@ -166,15 +176,21 @@ func renderNestedInsertRelTables(w io.Writer, item kvitem) error {
io.WriteString(w, `, `) io.WriteString(w, `, `)
} }
} else { } else {
// Render child foreign key columns if child-to-parent // Render tables needed to set values if child-to-parent
// relationship is one-to-many // relationship is one-to-many
for _, v := range item.items { for _, v := range item.items {
if v.relCP.Type == RelOneToMany { if v.relCP.Type == RelOneToMany {
if v._ctype > 0 {
io.WriteString(w, `"_x_`)
io.WriteString(w, v.relCP.Left.Table)
io.WriteString(w, `", `)
} else {
quoted(w, v.relCP.Left.Table) quoted(w, v.relCP.Left.Table)
io.WriteString(w, `, `) io.WriteString(w, `, `)
} }
} }
} }
}
return nil return nil
} }

View File

@ -12,68 +12,41 @@ func simpleInsert(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t 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{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") compileGQLToPSQL(t, gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func singleInsert(t *testing.T) { func singleInsert(t *testing.T) {
gql := `mutation { gql := `mutation {
product(id: 15, insert: $insert) { product(id: $id, insert: $insert) {
id id
name name
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT "t"."name", "t"."description", "t"."price", "t"."user_id" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) 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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"insert": json.RawMessage(` { "name": "my_name", "price": 6.95, "description": "my_desc", "user_id": 5 }`), "insert": json.RawMessage(` { "name": "my_name", "price": 6.95, "description": "my_desc", "user_id": 5 }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "anon") compileGQLToPSQL(t, gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func bulkInsert(t *testing.T) { func bulkInsert(t *testing.T) {
gql := `mutation { gql := `mutation {
product(name: "test", id: 15, insert: $insert) { product(name: "test", id: $id, insert: $insert) {
id id
name name
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) 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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"insert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`), "insert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "anon") compileGQLToPSQL(t, gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func simpleInsertWithPresets(t *testing.T) { func simpleInsertWithPresets(t *testing.T) {
@ -83,20 +56,11 @@ func simpleInsertWithPresets(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`), "data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") compileGQLToPSQL(t, gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func nestedInsertManyToMany(t *testing.T) { func nestedInsertManyToMany(t *testing.T) {
@ -118,10 +82,6 @@ func nestedInsertManyToMany(t *testing.T) {
} }
}` }`
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "product_id", "customer_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "products"."id", "customers"."id" FROM "_sg_input" i, "products", "customers", json_populate_record(NULL::purchases, i.j) t RETURNING *) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" 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 row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products", json_populate_record(NULL::purchases, i.j) t RETURNING *) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" 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 row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(` { "data": json.RawMessage(` {
"sale_type": "bought", "sale_type": "bought",
@ -139,16 +99,7 @@ func nestedInsertManyToMany(t *testing.T) {
`), `),
} }
for i := 0; i < 1000; i++ { compileGQLToPSQL(t, gql, vars, "admin")
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql1 && string(resSQL) != sql2 {
t.Fatal(errNotExpected)
}
}
} }
func nestedInsertOneToMany(t *testing.T) { func nestedInsertOneToMany(t *testing.T) {
@ -165,8 +116,6 @@ func nestedInsertOneToMany(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j->'product') t 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"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"email": "thedude@rug.com", "email": "thedude@rug.com",
@ -182,14 +131,7 @@ func nestedInsertOneToMany(t *testing.T) {
}`), }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "admin") compileGQLToPSQL(t, gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func nestedInsertOneToOne(t *testing.T) { func nestedInsertOneToOne(t *testing.T) {
@ -205,8 +147,6 @@ func nestedInsertOneToOne(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) 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", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"name": "Apple", "name": "Apple",
@ -225,14 +165,7 @@ func nestedInsertOneToOne(t *testing.T) {
}`), }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "admin") compileGQLToPSQL(t, gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func nestedInsertOneToManyWithConnect(t *testing.T) { func nestedInsertOneToManyWithConnect(t *testing.T) {
@ -249,8 +182,6 @@ func nestedInsertOneToManyWithConnect(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id"FROM "users" WHERE ("products"."id" = '5') RETURNING "products".*) 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"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"email": "thedude@rug.com", "email": "thedude@rug.com",
@ -263,14 +194,7 @@ func nestedInsertOneToManyWithConnect(t *testing.T) {
}`), }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "admin") compileGQLToPSQL(t, gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func nestedInsertOneToOneWithConnect(t *testing.T) { func nestedInsertOneToOneWithConnect(t *testing.T) {
@ -278,6 +202,10 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
product(insert: $data) { product(insert: $data) {
id id
name name
tags {
id
name
}
user { user {
id id
full_name full_name
@ -286,8 +214,6 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."id" = '5' LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) 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", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"name": "Apple", "name": "Apple",
@ -300,15 +226,36 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
}`), }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "admin") compileGQLToPSQL(t, gql, vars, "admin")
if err != nil {
t.Fatal(err)
} }
if string(resSQL) != sql { func nestedInsertOneToOneWithConnectArray(t *testing.T) {
t.Fatal(errNotExpected) gql := `mutation {
product(insert: $data) {
id
name
user {
id
full_name
email
} }
} }
}`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{
"name": "Apple",
"price": 1.25,
"created_at": "now",
"updated_at": "now",
"user": {
"connect": { "id": [1,2] }
}
}`),
}
compileGQLToPSQL(t, gql, vars, "admin")
}
func TestCompileInsert(t *testing.T) { func TestCompileInsert(t *testing.T) {
t.Run("simpleInsert", simpleInsert) t.Run("simpleInsert", simpleInsert)
@ -320,4 +267,5 @@ func TestCompileInsert(t *testing.T) {
t.Run("nestedInsertOneToOne", nestedInsertOneToOne) t.Run("nestedInsertOneToOne", nestedInsertOneToOne)
t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect) t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect)
t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect) t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect)
t.Run("nestedInsertOneToOneWithConnectArray", nestedInsertOneToOneWithConnectArray)
} }

View File

@ -77,7 +77,7 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables
root.Where = nil root.Where = nil
root.Args = nil root.Args = nil
return c.compileQuery(qc, w) return c.compileQuery(qc, w, vars)
} }
type kvitem struct { type kvitem struct {
@ -101,6 +101,9 @@ type renitem struct {
data map[string]json.RawMessage data map[string]json.RawMessage
} }
// TODO: Handle cases where a column name matches the child table name
// the child path needs to be exluded in the json sent to insert or update
func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
var data map[string]json.RawMessage var data map[string]json.RawMessage
var array bool var array bool
@ -124,9 +127,6 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
if v[0] != '{' && v[0] != '[' { if v[0] != '{' && v[0] != '[' {
continue continue
} }
if _, ok := item.ti.ColMap[k]; ok {
continue
}
// Get child-to-parent relationship // Get child-to-parent relationship
relCP, err := c.schema.GetRel(k, item.key) relCP, err := c.schema.GetRel(k, item.key)
@ -152,13 +152,9 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
id++ id++
} }
} else {
ti, err := c.schema.GetTable(k)
if err != nil {
return err
}
// Get parent-to-child relationship // Get parent-to-child relationship
relPC, err := c.schema.GetRel(item.key, k) } else if relPC, err := c.schema.GetRel(item.key, k); err == nil {
ti, err := c.schema.GetTable(k)
if err != nil { if err != nil {
return err return err
} }
@ -277,7 +273,11 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
io.WriteString(w, ` SET `) io.WriteString(w, ` SET `)
quoted(w, item.relPC.Right.Col) quoted(w, item.relPC.Right.Col)
io.WriteString(w, ` = `) io.WriteString(w, ` = `)
// When setting the id of the connected table in a one-to-many setting
// we always overwrite the value including for array columns
colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col) colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col)
io.WriteString(w, ` FROM `) io.WriteString(w, ` FROM `)
quoted(w, item.relPC.Left.Table) quoted(w, item.relPC.Left.Table)
io.WriteString(w, ` WHERE`) io.WriteString(w, ` WHERE`)
@ -290,7 +290,7 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
} else { } else {
io.WriteString(w, ` (`) io.WriteString(w, ` (`)
} }
if err := renderKVItemWhere(w, v); err != nil { if err := renderWhereFromJSON(w, v, "connect", v.val); err != nil {
return err return err
} }
io.WriteString(w, `)`) io.WriteString(w, `)`)
@ -313,7 +313,19 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
quoted(w, item.ti.Name) quoted(w, item.ti.Name)
io.WriteString(w, ` SET `) io.WriteString(w, ` SET `)
quoted(w, item.relPC.Right.Col) quoted(w, item.relPC.Right.Col)
io.WriteString(w, ` = NULL`) io.WriteString(w, ` = `)
if item.relPC.Right.Array {
io.WriteString(w, ` array_remove(`)
quoted(w, item.relPC.Right.Col)
io.WriteString(w, `, `)
colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col)
io.WriteString(w, `)`)
} else {
io.WriteString(w, ` NULL`)
}
io.WriteString(w, ` FROM `) io.WriteString(w, ` FROM `)
quoted(w, item.relPC.Left.Table) quoted(w, item.relPC.Left.Table)
io.WriteString(w, ` WHERE`) io.WriteString(w, ` WHERE`)
@ -326,7 +338,7 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
} else { } else {
io.WriteString(w, ` (`) io.WriteString(w, ` (`)
} }
if err := renderKVItemWhere(w, v); err != nil { if err := renderWhereFromJSON(w, v, "disconnect", v.val); err != nil {
return err return err
} }
io.WriteString(w, `)`) io.WriteString(w, `)`)
@ -335,10 +347,11 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
} }
io.WriteString(w, ` RETURNING `) io.WriteString(w, ` RETURNING `)
quoted(w, item.ti.Name) quoted(w, item.ti.Name)
io.WriteString(w, `.*), `) io.WriteString(w, `.*)`)
} }
if connect && disconnect { if connect && disconnect {
io.WriteString(w, `, `)
quoted(w, item.ti.Name) quoted(w, item.ti.Name)
io.WriteString(w, ` AS (`) io.WriteString(w, ` AS (`)
io.WriteString(w, `SELECT * FROM `) io.WriteString(w, `SELECT * FROM `)
@ -433,7 +446,10 @@ func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer,
upsert, ok := vars[qc.ActionVar] upsert, ok := vars[qc.ActionVar]
if !ok { if !ok {
return 0, fmt.Errorf("Variable '%s' not defined", qc.ActionVar) return 0, fmt.Errorf("variable '%s' not defined", qc.ActionVar)
}
if len(upsert) == 0 {
return 0, fmt.Errorf("variable '%s' is empty", qc.ActionVar)
} }
if ti.PrimaryCol == nil { if ti.PrimaryCol == nil {
@ -507,18 +523,33 @@ func (c *compilerContext) renderConnectStmt(qc *qcode.QCode, w io.Writer,
rel := item.relPC rel := item.relPC
// Render only for parent-to-child relationship of one-to-one // Render only for parent-to-child relationship of one-to-one
// For this to work the child needs to found first so it's primary key
// can be set in the related column on the parent object.
// Eg. Create product and connect a user to it.
if rel.Type != RelOneToOne { if rel.Type != RelOneToOne {
return nil return nil
} }
io.WriteString(w, `, `) io.WriteString(w, `, "_x_`)
quoted(w, item.ti.Name) io.WriteString(c.w, item.ti.Name)
io.WriteString(c.w, ` AS (`) io.WriteString(c.w, `" AS (SELECT `)
io.WriteString(c.w, `SELECT * FROM `) if rel.Left.Array {
io.WriteString(w, `array_agg(DISTINCT `)
quoted(w, rel.Right.Col)
io.WriteString(w, `) AS `)
quoted(w, rel.Right.Col)
} else {
quoted(w, rel.Right.Col)
}
io.WriteString(c.w, ` FROM "_sg_input" i,`)
quoted(c.w, item.ti.Name) quoted(c.w, item.ti.Name)
io.WriteString(c.w, ` WHERE `) io.WriteString(c.w, ` WHERE `)
if err := renderKVItemWhere(c.w, item.kvitem); err != nil { if err := renderWhereFromJSON(c.w, item.kvitem, "connect", item.kvitem.val); err != nil {
return err return err
} }
io.WriteString(c.w, ` LIMIT 1)`) io.WriteString(c.w, ` LIMIT 1)`)
@ -532,50 +563,105 @@ func (c *compilerContext) renderDisconnectStmt(qc *qcode.QCode, w io.Writer,
rel := item.relPC rel := item.relPC
// Render only for parent-to-child relationship of one-to-one // Render only for parent-to-child relationship of one-to-one
// For this to work the child needs to found first so it's
// null value can beset in the related column on the parent object.
// Eg. Update product and diconnect the user from it.
if rel.Type != RelOneToOne { if rel.Type != RelOneToOne {
return nil return nil
} }
io.WriteString(w, `, `) io.WriteString(w, `, "_x_`)
quoted(w, item.ti.Name) io.WriteString(c.w, item.ti.Name)
io.WriteString(c.w, ` AS (`) io.WriteString(c.w, `" AS (`)
if rel.Right.Array {
io.WriteString(c.w, `SELECT `)
quoted(w, rel.Right.Col)
io.WriteString(c.w, ` FROM "_sg_input" i,`)
quoted(c.w, item.ti.Name)
io.WriteString(c.w, ` WHERE `)
if err := renderWhereFromJSON(c.w, item.kvitem, "connect", item.kvitem.val); err != nil {
return err
}
io.WriteString(c.w, ` LIMIT 1))`)
} else {
io.WriteString(c.w, `SELECT * FROM (VALUES(NULL::`) io.WriteString(c.w, `SELECT * FROM (VALUES(NULL::`)
io.WriteString(w, rel.Right.col.Type) io.WriteString(w, rel.Right.col.Type)
io.WriteString(c.w, `)) AS LOOKUP(`) io.WriteString(c.w, `)) AS LOOKUP(`)
quoted(w, rel.Right.Col) quoted(w, rel.Right.Col)
io.WriteString(c.w, `))`) io.WriteString(c.w, `))`)
}
return nil return nil
} }
func renderKVItemWhere(w io.Writer, item kvitem) error { func renderWhereFromJSON(w io.Writer, item kvitem, key string, val []byte) error {
return renderWhereFromJSON(w, item.ti.Name, item.val)
}
func renderWhereFromJSON(w io.Writer, table string, val []byte) error {
var kv map[string]json.RawMessage var kv map[string]json.RawMessage
ti := item.ti
if err := json.Unmarshal(val, &kv); err != nil { if err := json.Unmarshal(val, &kv); err != nil {
return err return err
} }
i := 0 i := 0
for k, v := range kv { for k, v := range kv {
col, ok := ti.ColMap[k]
if !ok {
continue
}
if i != 0 { if i != 0 {
io.WriteString(w, ` AND `) io.WriteString(w, ` AND `)
} }
colWithTable(w, table, k)
io.WriteString(w, ` = '`) if v[0] == '[' {
switch v[0] { colWithTable(w, ti.Name, k)
case '"':
w.Write(v[1 : len(v)-1]) if col.Array {
default: io.WriteString(w, ` && `)
w.Write(v) } else {
io.WriteString(w, ` = `)
} }
io.WriteString(w, `'`)
io.WriteString(w, `ANY((select a::`)
io.WriteString(w, col.Type)
io.WriteString(w, ` AS list from json_array_elements_text(`)
renderPathJSON(w, item, key, k)
io.WriteString(w, `::json) AS a))`)
} else if col.Array {
io.WriteString(w, `(`)
renderPathJSON(w, item, key, k)
io.WriteString(w, `)::`)
io.WriteString(w, col.Type)
io.WriteString(w, ` = ANY(`)
colWithTable(w, ti.Name, k)
io.WriteString(w, `)`)
} else {
colWithTable(w, ti.Name, k)
io.WriteString(w, `= (`)
renderPathJSON(w, item, key, k)
io.WriteString(w, `)::`)
io.WriteString(w, col.Type)
}
i++ i++
} }
return nil return nil
} }
func renderPathJSON(w io.Writer, item kvitem, key1, key2 string) {
io.WriteString(w, `(i.j->`)
joinPath(w, item.path)
io.WriteString(w, `->'`)
io.WriteString(w, key1)
io.WriteString(w, `'->>'`)
io.WriteString(w, key2)
io.WriteString(w, `')`)
}
func renderCteName(w io.Writer, item kvitem) error { func renderCteName(w io.Writer, item kvitem) error {
io.WriteString(w, `"`) io.WriteString(w, `"`)
io.WriteString(w, item.ti.Name) io.WriteString(w, item.ti.Name)
@ -596,12 +682,6 @@ func renderCteNameWithSuffix(w io.Writer, item kvitem, suffix string) error {
return nil return nil
} }
func quoted(w io.Writer, identifier string) {
io.WriteString(w, `"`)
io.WriteString(w, identifier)
io.WriteString(w, `"`)
}
func joinPath(w io.Writer, path []string) { func joinPath(w io.Writer, path []string) {
for i := range path { for i := range path {
if i != 0 { if i != 0 {

View File

@ -13,20 +13,11 @@ func singleUpsert(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) 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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), "upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") compileGQLToPSQL(t, gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func singleUpsertWhere(t *testing.T) { func singleUpsertWhere(t *testing.T) {
@ -37,20 +28,11 @@ func singleUpsertWhere(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) WHERE (("products"."price") > 3) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) 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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), "upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") compileGQLToPSQL(t, gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func bulkUpsert(t *testing.T) { func bulkUpsert(t *testing.T) {
@ -61,20 +43,11 @@ func bulkUpsert(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) 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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`), "upsert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") compileGQLToPSQL(t, gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func delete(t *testing.T) { func delete(t *testing.T) {
@ -85,20 +58,11 @@ func delete(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") IS NOT DISTINCT FROM 1)) 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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), "update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") compileGQLToPSQL(t, gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
// func blockedInsert(t *testing.T) { // func blockedInsert(t *testing.T) {

View File

@ -1,6 +1,8 @@
package psql package psql
import ( import (
"fmt"
"io/ioutil"
"log" "log"
"os" "os"
"strings" "strings"
@ -11,11 +13,14 @@ import (
const ( const (
errNotExpected = "Generated SQL did not match what was expected" errNotExpected = "Generated SQL did not match what was expected"
headerMarker = "=== RUN"
commentMarker = "---"
) )
var ( var (
qcompile *qcode.Compiler qcompile *qcode.Compiler
pcompile *Compiler pcompile *Compiler
expected map[string][]string
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -128,92 +133,7 @@ func TestMain(m *testing.M) {
log.Fatal(err) log.Fatal(err)
} }
tables := []DBTable{ schema := getTestSchema()
DBTable{Name: "customers", Type: "table"},
DBTable{Name: "users", Type: "table"},
DBTable{Name: "products", Type: "table"},
DBTable{Name: "purchases", Type: "table"},
DBTable{Name: "tags", Type: "table"},
}
columns := [][]DBColumn{
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 5, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 6, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 8, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 9, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 10, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "avatar", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 5, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 6, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 8, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 9, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 10, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 11, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "name", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "description", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "price", Type: "numeric(7,2)", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 5, Name: "user_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "users", FKeyColID: []int16{1}},
DBColumn{ID: 6, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 8, Name: "tsv", Type: "tsvector", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 9, Name: "tags", Type: "text[]", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tags", FKeyColID: []int16{3}, Array: true}},
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "customer_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "customers", FKeyColID: []int16{1}},
DBColumn{ID: 3, Name: "product_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "products", FKeyColID: []int16{1}},
DBColumn{ID: 4, Name: "sale_type", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 5, Name: "quantity", Type: "integer", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 6, Name: "due_date", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "returned", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "name", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "slug", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false}},
}
for i := range tables {
tables[i].Key = strings.ToLower(tables[i].Name)
for n := range columns[i] {
columns[i][n].Key = strings.ToLower(columns[i][n].Name)
}
}
schema := &DBSchema{
ver: 110000,
t: make(map[string]*DBTableInfo),
rm: make(map[string]map[string]*DBRel),
}
aliases := map[string][]string{
"users": []string{"mes"},
}
for i, t := range tables {
err := schema.addTable(t, columns[i], aliases)
if err != nil {
log.Fatal(err)
}
}
for i, t := range tables {
err := schema.updateRelationships(t, columns[i])
if err != nil {
log.Fatal(err)
}
}
vars := NewVariables(map[string]string{ vars := NewVariables(map[string]string{
"admin_account_id": "5", "admin_account_id": "5",
@ -224,21 +144,94 @@ func TestMain(m *testing.M) {
Vars: vars, Vars: vars,
}) })
expected = make(map[string][]string)
b, err := ioutil.ReadFile("tests.sql")
if err != nil {
log.Fatal(err)
}
text := string(b)
lines := strings.Split(text, "\n")
var h string
for _, v := range lines {
switch {
case strings.HasPrefix(v, headerMarker):
h = strings.TrimSpace(v[len(headerMarker):])
case strings.HasPrefix(v, commentMarker):
break
default:
v := strings.TrimSpace(v)
if len(v) != 0 {
expected[h] = append(expected[h], v)
}
}
}
os.Exit(m.Run()) os.Exit(m.Run())
} }
func compileGQLToPSQL(gql string, vars Variables, role string) ([]byte, error) { func compileGQLToPSQL(t *testing.T, gql string, vars Variables, role string) {
generateTestFile := false
if generateTestFile {
var sqlStmts []string
for i := 0; i < 100; i++ {
qc, err := qcompile.Compile([]byte(gql), role) qc, err := qcompile.Compile([]byte(gql), role)
if err != nil { if err != nil {
return nil, err t.Fatal(err)
}
_, sqlB, err := pcompile.CompileEx(qc, vars)
if err != nil {
t.Fatal(err)
}
sql := string(sqlB)
match := false
for _, s := range sqlStmts {
if sql == s {
match = true
break
}
}
if !match {
s := string(sql)
sqlStmts = append(sqlStmts, s)
fmt.Println(s)
}
}
return
}
for i := 0; i < 200; i++ {
qc, err := qcompile.Compile([]byte(gql), role)
if err != nil {
t.Fatal(err)
} }
_, sqlStmt, err := pcompile.CompileEx(qc, vars) _, sqlStmt, err := pcompile.CompileEx(qc, vars)
if err != nil { if err != nil {
return nil, err t.Fatal(err)
} }
//fmt.Println(string(sqlStmt)) failed := true
return sqlStmt, nil for _, sql := range expected[t.Name()] {
if string(sqlStmt) == sql {
failed = false
}
}
if failed {
fmt.Println(string(sqlStmt))
t.Fatal(errNotExpected)
}
}
} }

View File

@ -30,7 +30,10 @@ type Compiler struct {
} }
func NewCompiler(conf Config) *Compiler { func NewCompiler(conf Config) *Compiler {
return &Compiler{conf.Schema, conf.Vars} return &Compiler{
schema: conf.Schema,
vars: conf.Vars,
}
} }
func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error { func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error {
@ -65,7 +68,7 @@ func (co *Compiler) CompileEx(qc *qcode.QCode, vars Variables) (uint32, []byte,
func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) { func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
switch qc.Type { switch qc.Type {
case qcode.QTQuery: case qcode.QTQuery:
return co.compileQuery(qc, w) return co.compileQuery(qc, w, vars)
case qcode.QTInsert, qcode.QTUpdate, qcode.QTDelete, qcode.QTUpsert: case qcode.QTInsert, qcode.QTUpdate, qcode.QTDelete, qcode.QTUpsert:
return co.compileMutation(qc, w, vars) return co.compileMutation(qc, w, vars)
} }
@ -73,21 +76,22 @@ func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint3
return 0, fmt.Errorf("Unknown operation type %d", qc.Type) return 0, fmt.Errorf("Unknown operation type %d", qc.Type)
} }
func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) { func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
if len(qc.Selects) == 0 { if len(qc.Selects) == 0 {
return 0, errors.New("empty query") return 0, errors.New("empty query")
} }
c := &compilerContext{w, qc.Selects, co} c := &compilerContext{w, qc.Selects, co}
multiRoot := (len(qc.Roots) > 1)
st := NewIntStack() st := NewIntStack()
i := 0
if multiRoot { io.WriteString(c.w, `SELECT json_build_object(`)
io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `) for _, id := range qc.Roots {
root := &qc.Selects[id]
for i, id := range qc.Roots { if root.SkipRender {
root := qc.Selects[id] continue
}
st.Push(root.ID + closeBlock) st.Push(root.ID + closeBlock)
st.Push(root.ID) st.Push(root.ID)
@ -96,31 +100,14 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
io.WriteString(c.w, `, `) io.WriteString(c.w, `, `)
} }
io.WriteString(c.w, `"sel_`) c.renderRootSelect(root)
int2string(c.w, root.ID) i++
io.WriteString(c.w, `"."json_`)
int2string(c.w, root.ID)
io.WriteString(c.w, `"`)
alias(c.w, root.FieldName)
} }
io.WriteString(c.w, ` FROM `) io.WriteString(c.w, `) as "__root" FROM `)
} else { if i == 0 {
root := qc.Selects[0] return 0, errors.New("all tables skipped. cannot render query")
io.WriteString(c.w, `SELECT json_object_agg(`)
io.WriteString(c.w, `'`)
io.WriteString(c.w, root.FieldName)
io.WriteString(c.w, `', `)
io.WriteString(c.w, `json_`)
int2string(c.w, root.ID)
st.Push(root.ID + closeBlock)
st.Push(root.ID)
io.WriteString(c.w, `) FROM `)
} }
var ignored uint32 var ignored uint32
@ -135,22 +122,22 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
if id < closeBlock { if id < closeBlock {
sel := &c.s[id] sel := &c.s[id]
if sel.ParentID == -1 {
io.WriteString(c.w, `(`)
}
ti, err := c.schema.GetTable(sel.Name) ti, err := c.schema.GetTable(sel.Name)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if sel.ParentID != -1 { if sel.ParentID == -1 {
if err = c.renderLateralJoin(sel); err != nil { io.WriteString(c.w, `(`)
return 0, err } else {
} c.renderLateralJoin(sel)
} }
skipped, err := c.renderSelect(sel, ti) if !ti.Singular {
c.renderPluralSelect(sel, ti)
}
skipped, err := c.renderSelect(sel, ti, vars)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -161,6 +148,9 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
continue continue
} }
child := &c.s[cid] child := &c.s[cid]
if child.SkipRender {
continue
}
st.Push(child.ID + closeBlock) st.Push(child.ID + closeBlock)
st.Push(child.ID) st.Push(child.ID)
@ -174,40 +164,94 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
return 0, err return 0, err
} }
err = c.renderSelectClose(sel, ti) if !ti.Singular {
if err != nil { io.WriteString(c.w, `)`)
return 0, err aliasWithID(c.w, "__sel", sel.ID)
} }
if sel.ParentID != -1 { if sel.ParentID == -1 {
if err = c.renderLateralJoinClose(sel); err != nil {
return 0, err
}
} else {
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
aliasWithID(c.w, `sel`, sel.ID) aliasWithID(c.w, "__sel", sel.ID)
if st.Len() != 0 { if st.Len() != 0 {
io.WriteString(c.w, `, `) io.WriteString(c.w, `, `)
} }
} else {
c.renderLateralJoinClose(sel)
} }
if len(sel.Args) != 0 { if len(sel.Args) != 0 {
i := 0
for _, v := range sel.Args { for _, v := range sel.Args {
qcode.FreeNode(v) qcode.FreeNode(v, 500)
i++
} }
} }
} }
} }
if multiRoot {
io.WriteString(c.w, `) AS "json_root"`)
}
return ignored, nil return ignored, nil
} }
func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (uint32, []*qcode.Column) { func (c *compilerContext) renderPluralSelect(sel *qcode.Select, ti *DBTableInfo) error {
io.WriteString(c.w, `SELECT coalesce(json_agg("__sel_`)
int2string(c.w, sel.ID)
io.WriteString(c.w, `"."json"), '[]') as "json"`)
if sel.Paging.Type != qcode.PtOffset {
n := 0
// check if primary key already included in order by
// query argument
for _, ob := range sel.OrderBy {
if ob.Col == ti.PrimaryCol.Key {
n = 1
break
}
}
if n == 1 {
n = len(sel.OrderBy)
} else {
n = len(sel.OrderBy) + 1
}
io.WriteString(c.w, `, CONCAT_WS(','`)
for i := 0; i < n; i++ {
io.WriteString(c.w, `, max("__cur_`)
int2string(c.w, int32(i))
io.WriteString(c.w, `")`)
}
io.WriteString(c.w, `) as "cursor"`)
}
io.WriteString(c.w, ` FROM (`)
return nil
}
func (c *compilerContext) renderRootSelect(sel *qcode.Select) error {
io.WriteString(c.w, `'`)
io.WriteString(c.w, sel.FieldName)
io.WriteString(c.w, `', `)
io.WriteString(c.w, `"__sel_`)
int2string(c.w, sel.ID)
io.WriteString(c.w, `"."json"`)
if sel.Paging.Type != qcode.PtOffset {
io.WriteString(c.w, `, '`)
io.WriteString(c.w, sel.FieldName)
io.WriteString(c.w, `_cursor', `)
io.WriteString(c.w, `"__sel_`)
int2string(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 var skipped uint32
cols := make([]*qcode.Column, 0, len(sel.Cols)) cols := make([]*qcode.Column, 0, len(sel.Cols))
@ -217,168 +261,206 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
colmap[sel.Cols[i].Name] = struct{}{} colmap[sel.Cols[i].Name] = struct{}{}
} }
for i := range sel.OrderBy {
colmap[sel.OrderBy[i].Col] = struct{}{}
}
if sel.Paging.Type != qcode.PtOffset {
colmap[ti.PrimaryCol.Key] = struct{}{}
addPrimaryKey := true
for _, ob := range sel.OrderBy {
if ob.Col == ti.PrimaryCol.Key {
addPrimaryKey = false
break
}
}
if addPrimaryKey {
ob := &qcode.OrderBy{Col: ti.PrimaryCol.Name, Order: qcode.OrderAsc}
if sel.Paging.Type == qcode.PtBackward {
ob.Order = qcode.OrderDesc
}
sel.OrderBy = append(sel.OrderBy, ob)
}
}
if sel.Paging.Cursor {
c.addSeekPredicate(sel)
}
for _, id := range sel.Children { for _, id := range sel.Children {
child := &c.s[id] child := &c.s[id]
rel, err := c.schema.GetRel(child.Name, ti.Name) rel, err := c.schema.GetRel(child.Name, ti.Name)
if err != nil { if err != nil {
skipped |= (1 << uint(id)) return 0, nil, err
continue //skipped |= (1 << uint(id))
//continue
} }
switch rel.Type { switch rel.Type {
case RelOneToOne, RelOneToMany: case RelOneToOne, RelOneToMany:
if _, ok := colmap[rel.Right.Col]; !ok { if _, ok := colmap[rel.Right.Col]; !ok {
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Right.Col, FieldName: rel.Right.Col}) cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Right.Col, FieldName: rel.Right.Col})
}
colmap[rel.Right.Col] = struct{}{} colmap[rel.Right.Col] = struct{}{}
}
case RelOneToManyThrough: case RelOneToManyThrough:
if _, ok := colmap[rel.Left.Col]; !ok { if _, ok := colmap[rel.Left.Col]; !ok {
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Left.Col}) cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Left.Col})
}
colmap[rel.Left.Col] = struct{}{} colmap[rel.Left.Col] = struct{}{}
}
case RelEmbedded:
if _, ok := colmap[rel.Left.Col]; !ok {
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Left.Col})
colmap[rel.Left.Col] = struct{}{}
}
case RelRemote: case RelRemote:
if _, ok := colmap[rel.Left.Col]; !ok { if _, ok := colmap[rel.Left.Col]; !ok {
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Right.Col}) cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Right.Col})
}
colmap[rel.Left.Col] = struct{}{} colmap[rel.Left.Col] = struct{}{}
skipped |= (1 << uint(id)) skipped |= (1 << uint(id))
}
default: default:
skipped |= (1 << uint(id)) return 0, nil, fmt.Errorf("unknown relationship %s", rel)
//skipped |= (1 << uint(id))
} }
} }
return skipped, cols return skipped, cols, nil
} }
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint32, error) { // This
skipped, childCols := c.processChildren(sel, ti) // (A, B, C) >= (X, Y, Z)
hasOrder := len(sel.OrderBy) != 0 //
// Becomes
// (A > X)
// OR ((A = X) AND (B > Y))
// OR ((A = X) AND (B = Y) AND (C > Z))
// OR ((A = X) AND (B = Y) AND (C = Z))
// SELECT func (c *compilerContext) addSeekPredicate(sel *qcode.Select) error {
if !ti.Singular { var or, and *qcode.Exp
//fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Name)
io.WriteString(c.w, `SELECT coalesce(json_agg("`)
io.WriteString(c.w, "json_")
int2string(c.w, sel.ID)
io.WriteString(c.w, `"`)
if hasOrder { obLen := len(sel.OrderBy)
err := c.renderOrderBy(sel, ti)
if err != nil { if obLen > 1 {
return skipped, err or = qcode.NewFilter()
} or.Op = qcode.OpOr
} }
//fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Name) for i := 0; i < obLen; i++ {
io.WriteString(c.w, `), '[]')`) if i > 0 {
aliasWithID(c.w, "json", sel.ID) and = qcode.NewFilter()
io.WriteString(c.w, ` FROM (`) and.Op = qcode.OpAnd
} }
// ROW-TO-JSON for n, ob := range sel.OrderBy {
io.WriteString(c.w, `SELECT `) f := qcode.NewFilter()
f.Col = ob.Col
f.Type = qcode.ValRef
f.Table = "__cur"
f.Val = ob.Col
if len(sel.DistinctOn) != 0 { if obLen == 1 {
c.renderDistinctOn(sel, ti) qcode.AddFilter(sel, f)
} return nil
io.WriteString(c.w, `row_to_json((`)
//fmt.Fprintf(w, `SELECT "%d" FROM (SELECT `, c.sel.ID)
io.WriteString(c.w, `SELECT "json_row_`)
int2string(c.w, sel.ID)
io.WriteString(c.w, `" FROM (SELECT `)
// Combined column names
c.renderColumns(sel, ti)
c.renderRemoteRelColumns(sel, ti)
err := c.renderJoinedColumns(sel, ti, skipped)
if err != nil {
return skipped, err
}
//fmt.Fprintf(w, `) AS "%d"`, c.sel.ID)
io.WriteString(c.w, `)`)
aliasWithID(c.w, "json_row", sel.ID)
//fmt.Fprintf(w, `)) AS "%s"`, c.sel.Name)
io.WriteString(c.w, `))`)
aliasWithID(c.w, "json", sel.ID)
// END-ROW-TO-JSON
if hasOrder {
c.renderOrderByColumns(sel, ti)
}
// END-SELECT
// FROM (SELECT .... )
err = c.renderBaseSelect(sel, ti, childCols, skipped)
if err != nil {
return skipped, err
}
// END-FROM
return skipped, nil
}
func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo) error {
hasOrder := len(sel.OrderBy) != 0
if hasOrder {
err := c.renderOrderBy(sel, ti)
if err != nil {
return err
}
} }
switch { switch {
case ti.Singular: case i > 0 && n != i:
io.WriteString(c.w, ` LIMIT ('1') :: integer`) f.Op = qcode.OpEquals
case ob.Order == qcode.OrderDesc:
case len(sel.Paging.Limit) != 0: f.Op = qcode.OpLesserThan
//fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit)
io.WriteString(c.w, ` LIMIT ('`)
io.WriteString(c.w, sel.Paging.Limit)
io.WriteString(c.w, `') :: integer`)
case sel.Paging.NoLimit:
break
default: default:
io.WriteString(c.w, ` LIMIT ('20') :: integer`) f.Op = qcode.OpGreaterThan
} }
if len(sel.Paging.Offset) != 0 { if and != nil {
//fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, c.sel.Paging.Offset) and.Children = append(and.Children, f)
io.WriteString(c.w, `OFFSET ('`) } else {
io.WriteString(c.w, sel.Paging.Offset) or.Children = append(or.Children, f)
io.WriteString(c.w, `') :: integer`)
} }
if !ti.Singular { if n == i {
//fmt.Fprintf(w, `) AS "json_agg_%d"`, c.sel.ID) break
io.WriteString(c.w, `)`) }
aliasWithID(c.w, "json_agg", sel.ID)
} }
if and != nil {
or.Children = append(or.Children, and)
}
}
qcode.AddFilter(sel, or)
return nil return nil
} }
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) (uint32, error) {
var rel *DBRel
var err error
if sel.ParentID != -1 {
parent := c.s[sel.ParentID]
rel, err = c.schema.GetRel(ti.Name, parent.Name)
if err != nil {
return 0, err
}
}
skipped, childCols, err := c.initSelect(sel, ti, vars)
if err != nil {
return 0, err
}
// SELECT
io.WriteString(c.w, `SELECT json_build_object(`)
if err := c.renderColumns(sel, ti, skipped); err != nil {
return 0, err
}
io.WriteString(c.w, `) AS "json"`)
if sel.Paging.Type != qcode.PtOffset {
for i, ob := range sel.OrderBy {
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))
io.WriteString(c.w, `"`)
}
}
io.WriteString(c.w, ` FROM (`)
// FROM (SELECT .... )
err = c.renderBaseSelect(sel, ti, rel, childCols, skipped)
if err != nil {
return skipped, err
}
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID)
io.WriteString(c.w, `)`)
aliasWithID(c.w, ti.Name, sel.ID)
// END-FROM
return skipped, nil
}
func (c *compilerContext) renderLateralJoin(sel *qcode.Select) error { func (c *compilerContext) renderLateralJoin(sel *qcode.Select) error {
io.WriteString(c.w, ` LEFT OUTER JOIN LATERAL (`) io.WriteString(c.w, ` LEFT OUTER JOIN LATERAL (`)
return nil return nil
} }
func (c *compilerContext) renderLateralJoinClose(sel *qcode.Select) error { func (c *compilerContext) renderLateralJoinClose(sel *qcode.Select) error {
//fmt.Fprintf(w, `) AS "%s_%d_join" ON ('true')`, c.sel.Name, c.sel.ID)
io.WriteString(c.w, `) `) io.WriteString(c.w, `) `)
aliasWithIDSuffix(c.w, sel.Name, sel.ID, "_join") aliasWithID(c.w, "__sel", sel.ID)
io.WriteString(c.w, ` ON ('true')`) io.WriteString(c.w, ` ON ('true')`)
return nil return nil
} }
@ -418,7 +500,7 @@ func (c *compilerContext) renderJoinByName(table, parent string, id int32) error
return nil return nil
} }
func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) { func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error {
i := 0 i := 0
for _, col := range sel.Cols { for _, col := range sel.Cols {
n := funcPrefixLen(col.Name) n := funcPrefixLen(col.Name)
@ -442,15 +524,21 @@ func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) {
if i != 0 { if i != 0 {
io.WriteString(c.w, ", ") io.WriteString(c.w, ", ")
} }
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
//c.sel.Name, c.sel.ID, col.Name, col.FieldName) squoted(c.w, col.FieldName)
colWithTableIDAlias(c.w, ti.Name, sel.ID, col.Name, col.FieldName) io.WriteString(c.w, ", ")
colWithTableID(c.w, ti.Name, sel.ID, col.Name)
i++ i++
} }
i += c.renderRemoteRelColumns(sel, ti, i)
return c.renderJoinColumns(sel, ti, skipped, i)
} }
func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo) { func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo, colsRendered int) int {
i := 0 i := colsRendered
for _, id := range sel.Children { for _, id := range sel.Children {
child := &c.s[id] child := &c.s[id]
@ -462,195 +550,77 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableI
if i != 0 || len(sel.Cols) != 0 { if i != 0 || len(sel.Cols) != 0 {
io.WriteString(c.w, ", ") io.WriteString(c.w, ", ")
} }
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
//c.sel.Name, c.sel.ID, rel.Left.Col, rel.Right.Col) squoted(c.w, rel.Right.Col)
io.WriteString(c.w, ", ")
colWithTableID(c.w, ti.Name, sel.ID, rel.Left.Col) colWithTableID(c.w, ti.Name, sel.ID, rel.Left.Col)
alias(c.w, rel.Right.Col)
i++ i++
} }
return i
} }
func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error { func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32, colsRendered int) error {
colsRendered := len(sel.Cols) != 0 // columns previously rendered
i := colsRendered
for _, id := range sel.Children { for _, id := range sel.Children {
skipThis := hasBit(skipped, uint32(id)) if hasBit(skipped, uint32(id)) {
if colsRendered && !skipThis {
io.WriteString(c.w, ", ")
}
if skipThis {
continue continue
} }
childSel := &c.s[id] childSel := &c.s[id]
if childSel.SkipRender {
continue
}
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`, if i != 0 {
//s.Name, s.ID, s.Name, s.FieldName) io.WriteString(c.w, ", ")
//if cti.Singular { }
io.WriteString(c.w, `"`)
io.WriteString(c.w, childSel.Name) squoted(c.w, childSel.FieldName)
io.WriteString(c.w, `_`)
io.WriteString(c.w, `, "__sel_`)
int2string(c.w, childSel.ID) int2string(c.w, childSel.ID)
io.WriteString(c.w, `_join"."json_`) io.WriteString(c.w, `"."json"`)
int2string(c.w, childSel.ID)
io.WriteString(c.w, `" AS "`) if childSel.Paging.Type != qcode.PtOffset {
io.WriteString(c.w, `, '`)
io.WriteString(c.w, childSel.FieldName) io.WriteString(c.w, childSel.FieldName)
io.WriteString(c.w, `"`) io.WriteString(c.w, `_cursor', "__sel_`)
int2string(c.w, childSel.ID)
io.WriteString(c.w, `"."cursor"`)
}
i++
} }
return nil return nil
} }
func (c *compilerContext) renderBaseSelect(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, skipped uint32) error {
var groupBy []int isRoot := (rel == nil)
isRoot := sel.ParentID == -1
isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop) isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop)
isSearch := sel.Args["search"] != nil hasOrder := len(sel.OrderBy) != 0
isAgg := false
io.WriteString(c.w, ` FROM (SELECT `) if sel.Paging.Cursor {
c.renderCursorCTE(sel)
i := 0
for n, col := range sel.Cols {
cn := col.Name
_, isRealCol := ti.ColMap[cn]
if !isRealCol {
if isSearch {
switch {
case cn == "search_rank":
if len(sel.Allowed) != 0 {
if _, ok := sel.Allowed[cn]; !ok {
continue
}
}
if ti.TSVCol == nil {
return errors.New("no ts_vector column found")
}
cn = ti.TSVCol.Name
arg := sel.Args["search"]
if i != 0 {
io.WriteString(c.w, `, `)
}
//fmt.Fprintf(w, `ts_rank("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
//c.sel.Name, cn, arg.Val, col.Name)
io.WriteString(c.w, `ts_rank(`)
colWithTable(c.w, ti.Name, cn)
if c.schema.ver >= 110000 {
io.WriteString(c.w, `, websearch_to_tsquery('`)
} else {
io.WriteString(c.w, `, to_tsquery('`)
}
io.WriteString(c.w, arg.Val)
io.WriteString(c.w, `'))`)
alias(c.w, col.Name)
i++
case strings.HasPrefix(cn, "search_headline_"):
cn1 := cn[16:]
if len(sel.Allowed) != 0 {
if _, ok := sel.Allowed[cn1]; !ok {
continue
}
}
arg := sel.Args["search"]
if i != 0 {
io.WriteString(c.w, `, `)
}
//fmt.Fprintf(w, `ts_headline("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
//c.sel.Name, cn, arg.Val, col.Name)
io.WriteString(c.w, `ts_headline(`)
colWithTable(c.w, ti.Name, cn1)
if c.schema.ver >= 110000 {
io.WriteString(c.w, `, websearch_to_tsquery('`)
} else {
io.WriteString(c.w, `, to_tsquery('`)
}
io.WriteString(c.w, arg.Val)
io.WriteString(c.w, `'))`)
alias(c.w, col.Name)
i++
}
} else {
pl := funcPrefixLen(cn)
if pl == 0 {
if i != 0 {
io.WriteString(c.w, `, `)
}
//fmt.Fprintf(w, `'%s not defined' AS %s`, cn, col.Name)
io.WriteString(c.w, `'`)
io.WriteString(c.w, cn)
io.WriteString(c.w, ` not defined'`)
alias(c.w, col.Name)
i++
} else if sel.Functions {
cn1 := cn[pl:]
if len(sel.Allowed) != 0 {
if _, ok := sel.Allowed[cn1]; !ok {
continue
}
}
if i != 0 {
io.WriteString(c.w, `, `)
}
fn := cn[0 : pl-1]
isAgg = true
//fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Name, cn, col.Name)
io.WriteString(c.w, fn)
io.WriteString(c.w, `(`)
colWithTable(c.w, ti.Name, cn1)
io.WriteString(c.w, `)`)
alias(c.w, col.Name)
i++
}
}
} else {
groupBy = append(groupBy, n)
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, cn)
if i != 0 {
io.WriteString(c.w, `, `)
}
colWithTable(c.w, ti.Name, cn)
i++
}
} }
for _, col := range childCols { io.WriteString(c.w, `SELECT `)
if i != 0 {
io.WriteString(c.w, `, `) if len(sel.DistinctOn) != 0 {
c.renderDistinctOn(sel, ti)
} }
//fmt.Fprintf(w, `"%s"."%s"`, col.Table, col.Name) realColsRendered, isAgg, err := c.renderBaseColumns(sel, ti, childCols, skipped)
colWithTable(c.w, col.Table, col.Name) if err != nil {
i++ return err
} }
io.WriteString(c.w, ` FROM `) io.WriteString(c.w, ` FROM `)
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name) c.renderFrom(sel, ti, rel)
io.WriteString(c.w, `"`)
io.WriteString(c.w, ti.Name)
io.WriteString(c.w, `"`)
// if tn, ok := c.tmap[sel.Name]; ok {
// //fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Name)
// tableWithAlias(c.w, ti.Name, sel.Name)
// } else {
// //fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name)
// io.WriteString(c.w, `"`)
// io.WriteString(c.w, sel.Name)
// io.WriteString(c.w, `"`)
// }
if isRoot && isFil { if isRoot && isFil {
io.WriteString(c.w, ` WHERE (`) io.WriteString(c.w, ` WHERE (`)
@ -666,11 +636,9 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
} }
io.WriteString(c.w, ` WHERE (`) io.WriteString(c.w, ` WHERE (`)
if err := c.renderRelationship(sel, ti); err != nil { if err := c.renderRelationship(sel, ti); err != nil {
return err return err
} }
if isFil { if isFil {
io.WriteString(c.w, ` AND `) io.WriteString(c.w, ` AND `)
if err := c.renderWhere(sel, ti); err != nil { if err := c.renderWhere(sel, ti); err != nil {
@ -680,18 +648,20 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
} }
if isAgg { if isAgg && len(realColsRendered) != 0 {
if len(groupBy) != 0 {
io.WriteString(c.w, ` GROUP BY `) io.WriteString(c.w, ` GROUP BY `)
for i, id := range groupBy { for i, id := range realColsRendered {
if i != 0 { c.renderComma(i)
io.WriteString(c.w, `, `)
}
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, c.sel.Cols[id].Name) //fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, c.sel.Cols[id].Name)
colWithTable(c.w, ti.Name, sel.Cols[id].Name) colWithTable(c.w, ti.Name, sel.Cols[id].Name)
} }
} }
if hasOrder {
if err := c.renderOrderBy(sel, ti); err != nil {
return err
}
} }
switch { switch {
@ -718,30 +688,64 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
io.WriteString(c.w, `') :: integer`) io.WriteString(c.w, `') :: integer`)
} }
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID) return nil
}
func (c *compilerContext) renderFrom(sel *qcode.Select, ti *DBTableInfo, rel *DBRel) error {
if rel != nil && rel.Type == RelEmbedded {
// json_to_recordset('[{"a":1,"b":[1,2,3],"c":"bar"}, {"a":2,"b":[1,2,3],"c":"bar"}]') as x(a int, b text, d text);
io.WriteString(c.w, `"`)
io.WriteString(c.w, rel.Left.Table)
io.WriteString(c.w, `", `)
io.WriteString(c.w, ti.Type)
io.WriteString(c.w, `_to_recordset(`)
colWithTable(c.w, rel.Left.Table, rel.Right.Col)
io.WriteString(c.w, `) AS `)
io.WriteString(c.w, `"`)
io.WriteString(c.w, ti.Name)
io.WriteString(c.w, `"`)
io.WriteString(c.w, `(`)
for i, col := range ti.Columns {
if i != 0 {
io.WriteString(c.w, `, `)
}
io.WriteString(c.w, col.Name)
io.WriteString(c.w, ` `)
io.WriteString(c.w, col.Type)
}
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
aliasWithID(c.w, ti.Name, sel.ID)
} else {
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name)
io.WriteString(c.w, `"`)
io.WriteString(c.w, ti.Name)
io.WriteString(c.w, `"`)
}
if sel.Paging.Cursor {
io.WriteString(c.w, `, "__cur"`)
}
return nil return nil
} }
func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInfo) { func (c *compilerContext) renderCursorCTE(sel *qcode.Select) error {
colsRendered := len(sel.Cols) != 0 io.WriteString(c.w, `WITH "__cur" AS (SELECT `)
for i, ob := range sel.OrderBy {
for i := range sel.OrderBy { if i != 0 {
if colsRendered {
//io.WriteString(w, ", ")
io.WriteString(c.w, `, `) io.WriteString(c.w, `, `)
} }
io.WriteString(c.w, `a[`)
col := sel.OrderBy[i].Col int2string(c.w, int32(i+1))
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`, io.WriteString(c.w, `] as `)
//c.sel.Name, c.sel.ID, c, quoted(c.w, ob.Col)
//c.sel.Name, c.sel.ID, c)
colWithTableID(c.w, ti.Name, sel.ID, col)
io.WriteString(c.w, ` AS `)
tableIDColSuffix(c.w, sel.Name, sel.ID, col, "_ob")
} }
io.WriteString(c.w, ` FROM string_to_array('{{cursor}}', ',') as a) `)
return nil
} }
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error { func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
@ -807,7 +811,13 @@ func (c *compilerContext) renderRelationshipByName(table, parent string, id int3
io.WriteString(c.w, `) = (`) io.WriteString(c.w, `) = (`)
colWithTable(c.w, rel.Through, rel.Right.Col) colWithTable(c.w, rel.Through, rel.Right.Col)
} }
case RelEmbedded:
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
io.WriteString(c.w, `) = (`)
colWithTableID(c.w, parent, id, rel.Left.Col)
} }
io.WriteString(c.w, `))`) io.WriteString(c.w, `))`)
return nil return nil
@ -858,7 +868,6 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b
switch val.Op { switch val.Op {
case qcode.OpFalse: case qcode.OpFalse:
st.Push(val.Op) st.Push(val.Op)
qcode.FreeExp(val)
case qcode.OpAnd, qcode.OpOr: case qcode.OpAnd, qcode.OpOr:
st.Push(')') st.Push(')')
@ -869,12 +878,12 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b
} }
} }
st.Push('(') st.Push('(')
qcode.FreeExp(val)
case qcode.OpNot: case qcode.OpNot:
//fmt.Printf("1> %s %d %s %s\n", val.Op, len(val.Children), val.Children[0].Op, val.Children[1].Op)
st.Push(val.Children[0]) st.Push(val.Children[0])
st.Push(qcode.OpNot) st.Push(qcode.OpNot)
qcode.FreeExp(val)
default: default:
if !skipNested && len(val.NestedCols) != 0 { if !skipNested && len(val.NestedCols) != 0 {
@ -889,14 +898,13 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b
if err := c.renderOp(val, ti); err != nil { if err := c.renderOp(val, ti); err != nil {
return err return err
} }
qcode.FreeExp(val)
} }
} }
//qcode.FreeExp(val)
default: default:
return fmt.Errorf("12: unexpected value %v (%t)", intf, intf) return fmt.Errorf("12: unexpected value %v (%t)", intf, intf)
} }
} }
return nil return nil
@ -963,8 +971,12 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error {
switch ex.Op { switch ex.Op {
case qcode.OpEquals: case qcode.OpEquals:
io.WriteString(c.w, `IS NOT DISTINCT FROM`) io.WriteString(c.w, `=`)
case qcode.OpNotEquals: case qcode.OpNotEquals:
io.WriteString(c.w, `!=`)
case qcode.OpNotDistinct:
io.WriteString(c.w, `IS NOT DISTINCT FROM`)
case qcode.OpDistinct:
io.WriteString(c.w, `IS DISTINCT FROM`) io.WriteString(c.w, `IS DISTINCT FROM`)
case qcode.OpGreaterOrEquals: case qcode.OpGreaterOrEquals:
io.WriteString(c.w, `>=`) io.WriteString(c.w, `>=`)
@ -1027,23 +1039,24 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error {
io.WriteString(c.w, `((`) io.WriteString(c.w, `((`)
colWithTable(c.w, ti.Name, ti.TSVCol.Name) colWithTable(c.w, ti.Name, ti.TSVCol.Name)
if c.schema.ver >= 110000 { if c.schema.ver >= 110000 {
io.WriteString(c.w, `) @@ websearch_to_tsquery('`) io.WriteString(c.w, `) @@ websearch_to_tsquery('{{`)
} else { } else {
io.WriteString(c.w, `) @@ to_tsquery('`) io.WriteString(c.w, `) @@ to_tsquery('{{`)
} }
io.WriteString(c.w, ex.Val) io.WriteString(c.w, ex.Val)
io.WriteString(c.w, `'))`) io.WriteString(c.w, `}}'))`)
return nil return nil
default: default:
return fmt.Errorf("[Where] unexpected op code %d", ex.Op) return fmt.Errorf("[Where] unexpected op code %d", ex.Op)
} }
if ex.Type == qcode.ValList { switch {
case ex.Type == qcode.ValList:
c.renderList(ex) c.renderList(ex)
} else if col == nil { case col == nil:
return errors.New("no column found for expression value") return errors.New("no column found for expression value")
} else { default:
c.renderVal(ex, c.vars, col) c.renderVal(ex, c.vars, col)
} }
@ -1058,31 +1071,20 @@ func (c *compilerContext) renderOrderBy(sel *qcode.Select, ti *DBTableInfo) erro
io.WriteString(c.w, `, `) io.WriteString(c.w, `, `)
} }
ob := sel.OrderBy[i] ob := sel.OrderBy[i]
colWithTable(c.w, ti.Name, ob.Col)
switch ob.Order { switch ob.Order {
case qcode.OrderAsc: case qcode.OrderAsc:
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Name, sel.ID, ob.Col)
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
io.WriteString(c.w, ` ASC`) io.WriteString(c.w, ` ASC`)
case qcode.OrderDesc: case qcode.OrderDesc:
//fmt.Fprintf(w, `"%s_%d.ob.%s" DESC`, sel.Name, sel.ID, ob.Col)
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
io.WriteString(c.w, ` DESC`) io.WriteString(c.w, ` DESC`)
case qcode.OrderAscNullsFirst: case qcode.OrderAscNullsFirst:
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC NULLS FIRST`, sel.Name, sel.ID, ob.Col)
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
io.WriteString(c.w, ` ASC NULLS FIRST`) io.WriteString(c.w, ` ASC NULLS FIRST`)
case qcode.OrderDescNullsFirst: case qcode.OrderDescNullsFirst:
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS FIRST`, sel.Name, sel.ID, ob.Col)
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
io.WriteString(c.w, ` DESC NULLLS FIRST`) io.WriteString(c.w, ` DESC NULLLS FIRST`)
case qcode.OrderAscNullsLast: case qcode.OrderAscNullsLast:
//fmt.Fprintf(w, `"%s_%d.ob.%s ASC NULLS LAST`, sel.Name, sel.ID, ob.Col)
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
io.WriteString(c.w, ` ASC NULLS LAST`) io.WriteString(c.w, ` ASC NULLS LAST`)
case qcode.OrderDescNullsLast: case qcode.OrderDescNullsLast:
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Name, sel.ID, ob.Col)
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
io.WriteString(c.w, ` DESC NULLS LAST`) io.WriteString(c.w, ` DESC NULLS LAST`)
default: default:
return fmt.Errorf("13: unexpected value %v", ob.Order) return fmt.Errorf("13: unexpected value %v", ob.Order)
@ -1097,8 +1099,7 @@ func (c *compilerContext) renderDistinctOn(sel *qcode.Select, ti *DBTableInfo) {
if i != 0 { if i != 0 {
io.WriteString(c.w, `, `) io.WriteString(c.w, `, `)
} }
//fmt.Fprintf(w, `"%s_%d.ob.%s"`, c.sel.Name, c.sel.ID, c.sel.DistinctOn[i]) colWithTable(c.w, ti.Name, sel.DistinctOn[i])
tableIDColSuffix(c.w, ti.Name, sel.ID, sel.DistinctOn[i], "_ob")
} }
io.WriteString(c.w, `) `) io.WriteString(c.w, `) `)
} }
@ -1125,33 +1126,25 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *
io.WriteString(c.w, ` `) io.WriteString(c.w, ` `)
switch ex.Type { switch ex.Type {
case qcode.ValBool, qcode.ValInt, qcode.ValFloat:
if len(ex.Val) != 0 {
io.WriteString(c.w, ex.Val)
} else {
io.WriteString(c.w, `''`)
}
case qcode.ValStr:
io.WriteString(c.w, `'`)
io.WriteString(c.w, ex.Val)
io.WriteString(c.w, `'`)
case qcode.ValVar: case qcode.ValVar:
io.WriteString(c.w, `'`)
if val, ok := vars[ex.Val]; ok { if val, ok := vars[ex.Val]; ok {
io.WriteString(c.w, val) squoted(c.w, val)
} else { } else {
//fmt.Fprintf(w, `'{{%s}}'`, ex.Val) io.WriteString(c.w, ` '{{`)
io.WriteString(c.w, `{{`)
io.WriteString(c.w, ex.Val) io.WriteString(c.w, ex.Val)
io.WriteString(c.w, `}}`) io.WriteString(c.w, `}}'`)
} }
io.WriteString(c.w, `' :: `)
case qcode.ValRef:
colWithTable(c.w, ex.Table, ex.Col)
default:
squoted(c.w, ex.Val)
}
io.WriteString(c.w, ` :: `)
io.WriteString(c.w, col.Type) io.WriteString(c.w, col.Type)
} }
//io.WriteString(c.w, `)`)
}
func funcPrefixLen(fn string) int { func funcPrefixLen(fn string) int {
switch { switch {
@ -1200,15 +1193,6 @@ func aliasWithID(w io.Writer, alias string, id int32) {
io.WriteString(w, `"`) io.WriteString(w, `"`)
} }
func aliasWithIDSuffix(w io.Writer, alias string, id int32, suffix string) {
io.WriteString(w, ` AS "`)
io.WriteString(w, alias)
io.WriteString(w, `_`)
int2string(w, id)
io.WriteString(w, suffix)
io.WriteString(w, `"`)
}
func colWithTable(w io.Writer, table, col string) { func colWithTable(w io.Writer, table, col string) {
io.WriteString(w, `"`) io.WriteString(w, `"`)
io.WriteString(w, table) io.WriteString(w, table)
@ -1229,27 +1213,16 @@ func colWithTableID(w io.Writer, table string, id int32, col string) {
io.WriteString(w, `"`) io.WriteString(w, `"`)
} }
func colWithTableIDAlias(w io.Writer, table string, id int32, col, alias string) { func quoted(w io.Writer, identifier string) {
io.WriteString(w, `"`) io.WriteString(w, `"`)
io.WriteString(w, table) io.WriteString(w, identifier)
io.WriteString(w, `_`)
int2string(w, id)
io.WriteString(w, `"."`)
io.WriteString(w, col)
io.WriteString(w, `" AS "`)
io.WriteString(w, alias)
io.WriteString(w, `"`) io.WriteString(w, `"`)
} }
func tableIDColSuffix(w io.Writer, table string, id int32, col, suffix string) { func squoted(w io.Writer, identifier string) {
io.WriteString(w, `"`) io.WriteString(w, `'`)
io.WriteString(w, table) io.WriteString(w, identifier)
io.WriteString(w, `_`) io.WriteString(w, `'`)
int2string(w, id)
io.WriteString(w, `_`)
io.WriteString(w, col)
io.WriteString(w, suffix)
io.WriteString(w, `"`)
} }
const charset = "0123456789" const charset = "0123456789"

View File

@ -2,6 +2,7 @@ package psql
import ( import (
"bytes" "bytes"
"encoding/json"
"testing" "testing"
) )
@ -28,16 +29,41 @@ func withComplexArgs(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0" ORDER BY "products_0_price_ob" DESC), '[]') AS "json_0" FROM (SELECT DISTINCT ON ("products_0_price_ob") row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0", "products_0"."price" AS "products_0_price_ob" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."id") < 28) AND (("products"."id") >= 20)))) LIMIT ('30') :: integer) AS "products_0" ORDER BY "products_0_price_ob" DESC LIMIT ('30') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
} }
if string(resSQL) != sql { func withWhereAndList(t *testing.T) {
t.Fatal(errNotExpected) gql := `query {
products(
where: {
and: [
{ not: { id: { is_null: true } } },
{ price: { gt: 10 } },
] } ) {
id
name
price
} }
}`
compileGQLToPSQL(t, gql, nil, "user")
}
func withWhereIsNull(t *testing.T) {
gql := `query {
products(
where: {
and: {
not: { id: { is_null: true } },
price: { gt: 10 }
}}) {
id
name
price
}
}`
compileGQLToPSQL(t, gql, nil, "user")
} }
func withWhereMultiOr(t *testing.T) { func withWhereMultiOr(t *testing.T) {
@ -56,93 +82,23 @@ func withWhereMultiOr(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") < 20) OR (("products"."price") > 10) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func withWhereIsNull(t *testing.T) {
gql := `query {
products(
where: {
and: {
not: { id: { is_null: true } },
price: { gt: 10 }
}}) {
id
name
price
}
}`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") > 10) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func withWhereAndList(t *testing.T) {
gql := `query {
products(
where: {
and: [
{ not: { id: { is_null: true } } },
{ price: { gt: 10 } },
] } ) {
id
name
price
}
}`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") > 10) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func fetchByID(t *testing.T) { func fetchByID(t *testing.T) {
gql := `query { gql := `query {
product(id: 15) { product(id: $id) {
id id
name name
} }
}` }`
sql := `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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") = 15))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func searchQuery(t *testing.T) { func searchQuery(t *testing.T) {
gql := `query { gql := `query {
products(search: "ale") { products(search: $query) {
id id
name name
search_rank search_rank
@ -150,16 +106,7 @@ func searchQuery(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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"."search_rank" AS "search_rank", "products_0"."search_headline_description" AS "search_headline_description") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('ale')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('ale')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('ale'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "admin")
resSQL, err := compileGQLToPSQL(gql, nil, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func oneToMany(t *testing.T) { func oneToMany(t *testing.T) {
@ -173,16 +120,7 @@ func oneToMany(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND ((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func oneToManyReverse(t *testing.T) { func oneToManyReverse(t *testing.T) {
@ -196,20 +134,12 @@ func oneToManyReverse(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "users_1_join"."json_1" AS "users") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "users_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func oneToManyArray(t *testing.T) { func oneToManyArray(t *testing.T) {
gql := `query { gql := `
query {
product { product {
name name
price price
@ -224,19 +154,9 @@ func oneToManyArray(t *testing.T) {
name name
} }
} }
}
}` }`
sql := `SELECT row_to_json("json_root") FROM (SELECT "sel_0"."json_0" AS "tags", "sel_2"."json_2" AS "product" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "products_2"."name" AS "name", "products_2"."price" AS "price", "tags_3_join"."json_3" AS "tags") AS "json_row_2")) AS "json_2" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_3"), '[]') AS "json_3" FROM (SELECT row_to_json((SELECT "json_row_3" FROM (SELECT "tags_3"."id" AS "id", "tags_3"."name" AS "name") AS "json_row_3")) AS "json_3" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3" LIMIT ('20') :: integer) AS "json_agg_3") AS "tags_3_join" ON ('true') LIMIT ('1') :: integer) AS "sel_2", (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "tags_0"."name" AS "name", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0") AS "json_root"` compileGQLToPSQL(t, gql, nil, "admin")
resSQL, err := compileGQLToPSQL(gql, nil, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func manyToMany(t *testing.T) { func manyToMany(t *testing.T) {
@ -250,16 +170,7 @@ func manyToMany(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "customers_1_join"."json_1" AS "customers") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "json_row_1")) AS "json_1" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func manyToManyReverse(t *testing.T) { func manyToManyReverse(t *testing.T) {
@ -273,16 +184,7 @@ func manyToManyReverse(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('customers', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func aggFunction(t *testing.T) { func aggFunction(t *testing.T) {
@ -293,16 +195,7 @@ func aggFunction(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func aggFunctionBlockedByCol(t *testing.T) { func aggFunctionBlockedByCol(t *testing.T) {
@ -313,16 +206,7 @@ func aggFunctionBlockedByCol(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "anon")
resSQL, err := compileGQLToPSQL(gql, nil, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func aggFunctionDisabled(t *testing.T) { func aggFunctionDisabled(t *testing.T) {
@ -333,16 +217,7 @@ func aggFunctionDisabled(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "anon1")
resSQL, err := compileGQLToPSQL(gql, nil, "anon1")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func aggFunctionWithFilter(t *testing.T) { func aggFunctionWithFilter(t *testing.T) {
@ -353,16 +228,7 @@ func aggFunctionWithFilter(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."max_price" AS "max_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", max("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") > 10))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func syntheticTables(t *testing.T) { func syntheticTables(t *testing.T) {
@ -372,16 +238,7 @@ func syntheticTables(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('me', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT ) AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func queryWithVariables(t *testing.T) { func queryWithVariables(t *testing.T) {
@ -392,16 +249,7 @@ func queryWithVariables(t *testing.T) {
} }
}` }`
sql := `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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") IS NOT DISTINCT FROM '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint)))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func withWhereOnRelations(t *testing.T) { func withWhereOnRelations(t *testing.T) {
@ -418,16 +266,7 @@ func withWhereOnRelations(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > 3)))) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func multiRoot(t *testing.T) { func multiRoot(t *testing.T) {
@ -451,37 +290,68 @@ func multiRoot(t *testing.T) {
} }
}` }`
sql := `SELECT row_to_json("json_root") FROM (SELECT "sel_0"."json_0" AS "customer", "sel_1"."json_1" AS "user", "sel_2"."json_2" AS "product" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "products_2"."id" AS "id", "products_2"."name" AS "name", "customers_3_join"."json_3" AS "customers", "customer_4_join"."json_4" AS "customer") AS "json_row_2")) AS "json_2" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_4" FROM (SELECT "customers_4"."email" AS "email") AS "json_row_4")) AS "json_4" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4" LIMIT ('1') :: integer) AS "customer_4_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_3"), '[]') AS "json_3" FROM (SELECT row_to_json((SELECT "json_row_3" FROM (SELECT "customers_3"."email" AS "email") AS "json_row_3")) AS "json_3" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3" LIMIT ('20') :: integer) AS "json_agg_3") AS "customers_3_join" ON ('true') LIMIT ('1') :: integer) AS "sel_2", (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "sel_1", (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0" LIMIT ('1') :: integer) AS "sel_0") AS "json_root"` compileGQLToPSQL(t, gql, nil, "user")
resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil {
t.Fatal(err)
} }
if string(resSQL) != sql { func withCursor(t *testing.T) {
t.Fatal(errNotExpected) gql := `query {
Products(
first: 20
after: $cursor
order_by: { price: desc }) {
Name
} }
}`
vars := map[string]json.RawMessage{
"cursor": json.RawMessage(`"0,1"`),
}
compileGQLToPSQL(t, gql, vars, "admin")
}
func jsonColumnAsTable(t *testing.T) {
gql := `query {
products {
id
name
tag_count {
count
tags {
name
}
}
}
}`
compileGQLToPSQL(t, gql, nil, "admin")
}
func skipUserIDForAnonRole(t *testing.T) {
gql := `query {
products {
id
name
user(where: { id: { eq: $user_id } }) {
id
email
}
}
}`
compileGQLToPSQL(t, gql, nil, "anon")
} }
func blockedQuery(t *testing.T) { func blockedQuery(t *testing.T) {
gql := `query { gql := `query {
user(id: 5, where: { id: { gt: 3 } }) { user(id: $id, where: { id: { gt: 3 } }) {
id id
full_name full_name
email email
} }
}` }`
sql := `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"."full_name" AS "full_name", "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` compileGQLToPSQL(t, gql, nil, "bad_dude")
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func blockedFunctions(t *testing.T) { func blockedFunctions(t *testing.T) {
@ -492,16 +362,7 @@ func blockedFunctions(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE (false) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` compileGQLToPSQL(t, gql, nil, "bad_dude")
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func TestCompileQuery(t *testing.T) { func TestCompileQuery(t *testing.T) {
@ -524,6 +385,9 @@ func TestCompileQuery(t *testing.T) {
t.Run("queryWithVariables", queryWithVariables) t.Run("queryWithVariables", queryWithVariables)
t.Run("withWhereOnRelations", withWhereOnRelations) t.Run("withWhereOnRelations", withWhereOnRelations)
t.Run("multiRoot", multiRoot) t.Run("multiRoot", multiRoot)
t.Run("jsonColumnAsTable", jsonColumnAsTable)
t.Run("withCursor", withCursor)
t.Run("skipUserIDForAnonRole", skipUserIDForAnonRole)
t.Run("blockedQuery", blockedQuery) t.Run("blockedQuery", blockedQuery)
t.Run("blockedFunctions", blockedFunctions) t.Run("blockedFunctions", blockedFunctions)
} }

View File

@ -15,6 +15,7 @@ type DBSchema struct {
type DBTableInfo struct { type DBTableInfo struct {
Name string Name string
Type string
Singular bool Singular bool
Columns []DBColumn Columns []DBColumn
PrimaryCol *DBColumn PrimaryCol *DBColumn
@ -29,6 +30,7 @@ const (
RelOneToOne RelType = iota + 1 RelOneToOne RelType = iota + 1
RelOneToMany RelOneToMany
RelOneToManyThrough RelOneToManyThrough
RelEmbedded
RelRemote RelRemote
) )
@ -51,7 +53,6 @@ type DBRel struct {
} }
func NewDBSchema(info *DBInfo, aliases map[string][]string) (*DBSchema, error) { func NewDBSchema(info *DBInfo, aliases map[string][]string) (*DBSchema, error) {
schema := &DBSchema{ schema := &DBSchema{
t: make(map[string]*DBTableInfo), t: make(map[string]*DBTableInfo),
rm: make(map[string]map[string]*DBRel), rm: make(map[string]map[string]*DBRel),
@ -83,6 +84,7 @@ func (s *DBSchema) addTable(
singular := flect.Singularize(t.Key) singular := flect.Singularize(t.Key)
s.t[singular] = &DBTableInfo{ s.t[singular] = &DBTableInfo{
Name: t.Name, Name: t.Name,
Type: t.Type,
Singular: true, Singular: true,
Columns: cols, Columns: cols,
ColMap: colmap, ColMap: colmap,
@ -92,6 +94,7 @@ func (s *DBSchema) addTable(
plural := flect.Pluralize(t.Key) plural := flect.Pluralize(t.Key)
s.t[plural] = &DBTableInfo{ s.t[plural] = &DBTableInfo{
Name: t.Name, Name: t.Name,
Type: t.Type,
Singular: false, Singular: false,
Columns: cols, Columns: cols,
ColMap: colmap, ColMap: colmap,
@ -136,20 +139,46 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
return fmt.Errorf("invalid foreign key table '%s'", ct) return fmt.Errorf("invalid foreign key table '%s'", ct)
} }
for _, c := range cols { for i := range cols {
if len(c.FKeyTable) == 0 || len(c.FKeyColID) == 0 { c := cols[i]
if len(c.FKeyTable) == 0 {
continue continue
} }
// Foreign key column name // Foreign key column name
ft := strings.ToLower(c.FKeyTable) ft := strings.ToLower(c.FKeyTable)
fcid := c.FKeyColID[0]
ti, ok := s.t[ft] ti, ok := s.t[ft]
if !ok { if !ok {
return fmt.Errorf("invalid foreign key table '%s'", ft) return fmt.Errorf("invalid foreign key table '%s'", ft)
} }
// This is an embedded relationship like when a json/jsonb column
// is exposed as a table
if c.Name == c.FKeyTable && len(c.FKeyColID) == 0 {
rel := &DBRel{Type: RelEmbedded}
rel.Left.col = cti.PrimaryCol
rel.Left.Table = cti.Name
rel.Left.Col = cti.PrimaryCol.Name
rel.Right.col = &c
rel.Right.Table = ti.Name
rel.Right.Col = c.Name
if err := s.SetRel(ft, ct, rel); err != nil {
return err
}
continue
}
if len(c.FKeyColID) == 0 {
continue
}
// Foreign key column id
fcid := c.FKeyColID[0]
fc, ok := ti.ColIDMap[fcid] fc, ok := ti.ColIDMap[fcid]
if !ok { if !ok {
return fmt.Errorf("invalid foreign key column id '%d' for table '%s'", return fmt.Errorf("invalid foreign key column id '%d' for table '%s'",
@ -188,10 +217,12 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
rel2 = &DBRel{Type: RelOneToMany} rel2 = &DBRel{Type: RelOneToMany}
} }
rel2.Left.col = fc
rel2.Left.Table = c.FKeyTable rel2.Left.Table = c.FKeyTable
rel2.Left.Col = fc.Name rel2.Left.Col = fc.Name
rel2.Left.Array = fc.Array rel2.Left.Array = fc.Array
rel2.Right.col = &c
rel2.Right.Table = t.Name rel2.Right.Table = t.Name
rel2.Right.Col = c.Name rel2.Right.Col = c.Name
rel2.Right.Array = c.Array rel2.Right.Array = c.Array
@ -249,9 +280,11 @@ func (s *DBSchema) updateSchemaOTMT(
rel1.Through = ti.Name rel1.Through = ti.Name
rel1.ColT = col2.Name rel1.ColT = col2.Name
rel1.Left.col = &col2
rel1.Left.Table = col2.FKeyTable rel1.Left.Table = col2.FKeyTable
rel1.Left.Col = fc2.Name rel1.Left.Col = fc2.Name
rel1.Right.col = &col1
rel1.Right.Table = ti.Name rel1.Right.Table = ti.Name
rel1.Right.Col = col1.Name rel1.Right.Col = col1.Name
@ -265,9 +298,11 @@ func (s *DBSchema) updateSchemaOTMT(
rel2.Through = ti.Name rel2.Through = ti.Name
rel2.ColT = col1.Name rel2.ColT = col1.Name
rel1.Left.col = fc1
rel2.Left.Table = col1.FKeyTable rel2.Left.Table = col1.FKeyTable
rel2.Left.Col = fc1.Name rel2.Left.Col = fc1.Name
rel1.Right.col = &col2
rel2.Right.Table = ti.Name rel2.Right.Table = ti.Name
rel2.Right.Col = col2.Name rel2.Right.Col = col2.Name

View File

@ -12,6 +12,8 @@ func (rt RelType) String() string {
return "one to many through" return "one to many through"
case RelRemote: case RelRemote:
return "remote" return "remote"
case RelEmbedded:
return "embedded"
} }
return "" return ""
} }

View File

@ -62,6 +62,20 @@ func GetDBInfo(db *pgxpool.Pool) (*DBInfo, error) {
return di, nil return di, nil
} }
func (di *DBInfo) AddTable(t DBTable, cols []DBColumn) {
t.ID = di.Tables[len(di.Tables)-1].ID
di.Tables = append(di.Tables, t)
di.colmap[t.Key] = make(map[string]*DBColumn, len(cols))
for i := range cols {
cols[i].ID = int16(i)
c := &cols[i]
di.colmap[t.Key][c.Key] = c
}
di.Columns = append(di.Columns, cols)
}
func (di *DBInfo) GetColumn(table, column string) (*DBColumn, bool) { func (di *DBInfo) GetColumn(table, column string) (*DBColumn, bool) {
v, ok := di.colmap[strings.ToLower(table)][strings.ToLower(column)] v, ok := di.colmap[strings.ToLower(table)][strings.ToLower(column)]
return v, ok return v, ok
@ -137,6 +151,7 @@ SELECT
pg_catalog.format_type(f.atttypid,f.atttypmod) AS type, pg_catalog.format_type(f.atttypid,f.atttypmod) AS type,
CASE CASE
WHEN f.attndims != 0 THEN true WHEN f.attndims != 0 THEN true
WHEN right(pg_catalog.format_type(f.atttypid,f.atttypmod), 2) = '[]' THEN true
ELSE false ELSE false
END AS array, END AS array,
CASE CASE
@ -152,7 +167,7 @@ SELECT
ELSE ''::text ELSE ''::text
END AS foreignkey, END AS foreignkey,
CASE CASE
WHEN p.contype = ('f'::char) THEN p.confkey WHEN p.contype = ('f'::char) THEN p.confkey::int2[]
ELSE ARRAY[]::int2[] ELSE ARRAY[]::int2[]
END AS foreignkey_fieldnum END AS foreignkey_fieldnum
FROM pg_attribute f FROM pg_attribute f
@ -161,7 +176,7 @@ FROM pg_attribute f
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey) LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
LEFT JOIN pg_class AS g ON p.confrelid = g.oid LEFT JOIN pg_class AS g ON p.confrelid = g.oid
WHERE c.relkind = ('r'::char) WHERE c.relkind IN ('r', 'v', 'm', 'f')
AND n.nspname = $1 -- Replace with Schema name AND n.nspname = $1 -- Replace with Schema name
AND c.relname = $2 -- Replace with table name AND c.relname = $2 -- Replace with table name
AND f.attnum > 0 AND f.attnum > 0
@ -229,3 +244,13 @@ ORDER BY id;`
return cols, nil return cols, nil
} }
// func GetValType(type string) qcode.ValType {
// switch {
// case "bigint", "integer", "smallint", "numeric", "bigserial":
// return qcode.ValInt
// case "double precision", "real":
// return qcode.ValFloat
// case ""
// }
// }

102
psql/test_schema.go Normal file
View File

@ -0,0 +1,102 @@
package psql
import (
"log"
"strings"
)
func getTestSchema() *DBSchema {
tables := []DBTable{
DBTable{Name: "customers", Type: "table"},
DBTable{Name: "users", Type: "table"},
DBTable{Name: "products", Type: "table"},
DBTable{Name: "purchases", Type: "table"},
DBTable{Name: "tags", Type: "table"},
DBTable{Name: "tag_count", Type: "json"},
}
columns := [][]DBColumn{
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 5, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 6, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 8, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 9, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 10, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "avatar", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 5, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 6, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 8, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 9, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 10, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 11, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "name", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "description", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "price", Type: "numeric(7,2)", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 5, Name: "user_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "users", FKeyColID: []int16{1}},
DBColumn{ID: 6, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 8, Name: "tsv", Type: "tsvector", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 9, Name: "tags", Type: "text[]", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tags", FKeyColID: []int16{3}, Array: true},
DBColumn{ID: 9, Name: "tag_count", Type: "json", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tag_count", FKeyColID: []int16{}}},
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "customer_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "customers", FKeyColID: []int16{1}},
DBColumn{ID: 3, Name: "product_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "products", FKeyColID: []int16{1}},
DBColumn{ID: 4, Name: "sale_type", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 5, Name: "quantity", Type: "integer", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 6, Name: "due_date", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "returned", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "name", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "slug", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{
DBColumn{ID: 1, Name: "tag_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tags", FKeyColID: []int16{1}},
DBColumn{ID: 2, Name: "count", Type: "int", NotNull: false, PrimaryKey: false, UniqueKey: false}},
}
for i := range tables {
tables[i].Key = strings.ToLower(tables[i].Name)
for n := range columns[i] {
columns[i][n].Key = strings.ToLower(columns[i][n].Name)
}
}
schema := &DBSchema{
ver: 110000,
t: make(map[string]*DBTableInfo),
rm: make(map[string]map[string]*DBRel),
}
aliases := map[string][]string{
"users": []string{"mes"},
}
for i, t := range tables {
err := schema.addTable(t, columns[i], aliases)
if err != nil {
log.Fatal(err)
}
}
for i, t := range tables {
err := schema.updateRelationships(t, columns[i])
if err != nil {
log.Fatal(err)
}
}
return schema
}

151
psql/tests.sql Normal file
View File

@ -0,0 +1,151 @@
=== RUN TestCompileInsert
=== RUN TestCompileInsert/simpleInsert
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id") AS "json" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0") AS "__sel_0"
=== RUN TestCompileInsert/singleInsert
WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT "t"."name", "t"."description", "t"."price", "t"."user_id" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
=== RUN TestCompileInsert/bulkInsert
WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_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 "t"."name", "t"."price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id") AS "json" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
=== RUN TestCompileInsert/nestedInsertManyToMany
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products", json_populate_record(NULL::purchases, i.j) t RETURNING *) SELECT json_build_object('purchase', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "__sel_1"."json", 'customer', "__sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" 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 "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "product_id", "customer_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "products"."id", "customers"."id" FROM "_sg_input" i, "products", "customers", json_populate_record(NULL::purchases, i.j) t RETURNING *) SELECT json_build_object('purchase', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "__sel_1"."json", 'customer', "__sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" 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 "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_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 "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j->'product') t RETURNING *) SELECT json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "__sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_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 "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" 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 "__sel_1" ON ('true')) AS "__sel_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 "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*) SELECT json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "__sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_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 "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json", 'tags', "__sel_2"."json") AS "json" 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(json_agg("__sel_2"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "tags_2"."id", 'name', "tags_2"."name") AS "json" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sel_2") AS "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" 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 "__sel_1" ON ('true')) AS "__sel_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 "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" 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 "__sel_1" ON ('true')) AS "__sel_0"
--- PASS: TestCompileInsert (0.02s)
--- PASS: TestCompileInsert/simpleInsert (0.00s)
--- PASS: TestCompileInsert/singleInsert (0.00s)
--- PASS: TestCompileInsert/bulkInsert (0.00s)
--- PASS: TestCompileInsert/simpleInsertWithPresets (0.00s)
--- PASS: TestCompileInsert/nestedInsertManyToMany (0.00s)
--- PASS: TestCompileInsert/nestedInsertOneToMany (0.00s)
--- PASS: TestCompileInsert/nestedInsertOneToOne (0.00s)
--- PASS: TestCompileInsert/nestedInsertOneToManyWithConnect (0.00s)
--- PASS: TestCompileInsert/nestedInsertOneToOneWithConnect (0.00s)
--- 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 "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
=== RUN TestCompileMutate/singleUpsertWhere
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) WHERE (("products"."price") > '3' :: numeric(7,2)) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
=== RUN TestCompileMutate/bulkUpsert
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_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 json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
--- PASS: TestCompileMutate (0.01s)
--- PASS: TestCompileMutate/singleUpsert (0.00s)
--- PASS: TestCompileMutate/singleUpsertWhere (0.00s)
--- PASS: TestCompileMutate/bulkUpsert (0.00s)
--- PASS: TestCompileMutate/delete (0.00s)
=== RUN TestCompileQuery
=== RUN TestCompileQuery/withComplexArgs
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price") AS "json" 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 "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/withWhereAndList
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price") AS "json" 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 "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/withWhereIsNull
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price") AS "json" 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 "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/withWhereMultiOr
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price") AS "json" 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 "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/fetchByID
SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" 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 "__sel_0"
=== RUN TestCompileQuery/searchQuery
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'search_rank', "products_0"."search_rank", 'search_headline_description', "products_0"."search_headline_description") AS "json" 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 "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/oneToMany
SELECT json_build_object('users', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "users_0"."email", 'products', "__sel_1"."json") AS "json" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_1"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_1"."name", 'price', "products_1"."price") AS "json" 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 "__sel_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/oneToManyReverse
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name", 'price', "products_0"."price", 'users', "__sel_1"."json") AS "json" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_1"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "users_1"."email") AS "json" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1") AS "__sel_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/oneToManyArray
SELECT json_build_object('tags', "__sel_0"."json", 'product', "__sel_2"."json") as "__root" FROM (SELECT json_build_object('name', "products_2"."name", 'price', "products_2"."price", 'tags', "__sel_3"."json") AS "json" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_3"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "tags_3"."id", 'name', "tags_3"."name") AS "json" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3") AS "__sel_3") AS "__sel_3" ON ('true')) AS "__sel_2", (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "tags_0"."name", 'product', "__sel_1"."json") AS "json" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('name', "products_1"."name") AS "json" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/manyToMany
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name", 'customers', "__sel_1"."json") AS "json" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_1"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "customers_1"."email", 'full_name', "customers_1"."full_name") AS "json" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1") AS "__sel_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/manyToManyReverse
SELECT json_build_object('customers', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "customers_0"."email", 'full_name', "customers_0"."full_name", 'products', "__sel_1"."json") AS "json" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_1"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_1"."name") AS "json" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sel_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/aggFunction
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name", 'count_price', "products_0"."count_price") AS "json" FROM (SELECT "products"."name", price("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/aggFunctionBlockedByCol
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name") AS "json" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/aggFunctionDisabled
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name") AS "json" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/aggFunctionWithFilter
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'max_price', "products_0"."max_price") AS "json" FROM (SELECT "products"."id", pri("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 "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/syntheticTables
SELECT json_build_object('me', "__sel_0"."json") as "__root" FROM (SELECT json_build_object() AS "json" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0") AS "__sel_0"
=== RUN TestCompileQuery/queryWithVariables
SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" 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 "__sel_0"
=== RUN TestCompileQuery/withWhereOnRelations
SELECT json_build_object('users', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "users_0"."id", 'email', "users_0"."email") AS "json" 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 "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/multiRoot
SELECT json_build_object('customer', "__sel_0"."json", 'user', "__sel_1"."json", 'product', "__sel_2"."json") as "__root" FROM (SELECT json_build_object('id', "products_2"."id", 'name', "products_2"."name", 'customers', "__sel_3"."json", 'customer', "__sel_4"."json") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT json_build_object('email', "customers_4"."email") AS "json" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4") AS "__sel_4" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_3"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "customers_3"."email") AS "json" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3") AS "__sel_3") AS "__sel_3" ON ('true')) AS "__sel_2", (SELECT json_build_object('id', "users_1"."id", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1") AS "__sel_1", (SELECT json_build_object('id', "customers_0"."id") AS "json" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0") AS "__sel_0"
=== RUN TestCompileQuery/jsonColumnAsTable
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'tag_count', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('count', "tag_count_1"."count", 'tags', "__sel_2"."json") AS "json" 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(json_agg("__sel_2"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "tags_2"."name") AS "json" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sel_2") AS "__sel_2" ON ('true')) AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/withCursor
SELECT json_build_object('products', "__sel_0"."json", 'products_cursor', "__sel_0"."cursor") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json", CONCAT_WS(',', max("__cur_0"), max("__cur_1")) as "cursor" FROM (SELECT json_build_object('name', "products_0"."name") AS "json", 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 "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/skipUserIDForAnonRole
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
=== RUN TestCompileQuery/blockedQuery
SELECT json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0") AS "__sel_0"
=== RUN TestCompileQuery/blockedFunctions
SELECT json_build_object('users', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "users_0"."email") AS "json" FROM (SELECT , "users"."email" FROM "users" WHERE (false) GROUP BY "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "__sel_0") AS "__sel_0"
--- PASS: TestCompileQuery (0.02s)
--- PASS: TestCompileQuery/withComplexArgs (0.00s)
--- PASS: TestCompileQuery/withWhereAndList (0.00s)
--- PASS: TestCompileQuery/withWhereIsNull (0.00s)
--- PASS: TestCompileQuery/withWhereMultiOr (0.00s)
--- PASS: TestCompileQuery/fetchByID (0.00s)
--- PASS: TestCompileQuery/searchQuery (0.00s)
--- PASS: TestCompileQuery/oneToMany (0.00s)
--- PASS: TestCompileQuery/oneToManyReverse (0.00s)
--- PASS: TestCompileQuery/oneToManyArray (0.00s)
--- PASS: TestCompileQuery/manyToMany (0.00s)
--- PASS: TestCompileQuery/manyToManyReverse (0.00s)
--- PASS: TestCompileQuery/aggFunction (0.00s)
--- PASS: TestCompileQuery/aggFunctionBlockedByCol (0.00s)
--- PASS: TestCompileQuery/aggFunctionDisabled (0.00s)
--- PASS: TestCompileQuery/aggFunctionWithFilter (0.00s)
--- PASS: TestCompileQuery/syntheticTables (0.00s)
--- PASS: TestCompileQuery/queryWithVariables (0.00s)
--- PASS: TestCompileQuery/withWhereOnRelations (0.00s)
--- PASS: TestCompileQuery/multiRoot (0.00s)
--- PASS: TestCompileQuery/jsonColumnAsTable (0.00s)
--- PASS: TestCompileQuery/withCursor (0.00s)
--- PASS: TestCompileQuery/skipUserIDForAnonRole (0.00s)
--- PASS: TestCompileQuery/blockedQuery (0.00s)
--- 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 "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE ((("products"."id") = '1' :: bigint) AND (("products"."id") = '{{id}}' :: bigint)) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
=== RUN TestCompileUpdate/simpleUpdateWithPresets
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id") AS "json" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
=== RUN TestCompileUpdate/nestedUpdateManyToMany
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = '{{id}}' :: bigint) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_build_object('purchase', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "__sel_1"."json", 'customer', "__sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" 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 "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = '{{id}}' :: bigint) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_build_object('purchase', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "__sel_1"."json", 'customer', "__sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" 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 "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_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 "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") = '8' :: bigint) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id"= ((i.j->'product'->'where'->>'id'))::bigint) RETURNING "products".*) SELECT json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "__sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_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 "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{id}}' :: bigint) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" 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 "__sel_1" ON ('true')) AS "__sel_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 "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) 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 json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "__sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_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 "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{product_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" 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 "__sel_1" ON ('true')) AS "__sel_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 "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{product_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" 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 "__sel_1" ON ('true')) AS "__sel_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 "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user_id', "products_0"."user_id") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
--- PASS: TestCompileUpdate (0.02s)
--- PASS: TestCompileUpdate/singleUpdate (0.00s)
--- PASS: TestCompileUpdate/simpleUpdateWithPresets (0.00s)
--- PASS: TestCompileUpdate/nestedUpdateManyToMany (0.00s)
--- PASS: TestCompileUpdate/nestedUpdateOneToMany (0.00s)
--- PASS: TestCompileUpdate/nestedUpdateOneToOne (0.00s)
--- PASS: TestCompileUpdate/nestedUpdateOneToManyWithConnect (0.00s)
--- PASS: TestCompileUpdate/nestedUpdateOneToOneWithConnect (0.00s)
--- PASS: TestCompileUpdate/nestedUpdateOneToOneWithDisconnect (0.00s)
PASS
ok github.com/dosco/super-graph/psql 0.716s

View File

@ -15,7 +15,10 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer,
update, ok := vars[qc.ActionVar] update, ok := vars[qc.ActionVar]
if !ok { if !ok {
return 0, fmt.Errorf("Variable '%s' not !defined", qc.ActionVar) return 0, fmt.Errorf("variable '%s' not !defined", qc.ActionVar)
}
if len(update) == 0 {
return 0, fmt.Errorf("variable '%s' is empty", qc.ActionVar)
} }
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`) io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
@ -125,10 +128,10 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re
if item.relPC.Type == RelOneToMany { if item.relPC.Type == RelOneToMany {
if conn, ok := item.data["where"]; ok { if conn, ok := item.data["where"]; ok {
io.WriteString(w, ` AND `) io.WriteString(w, ` AND `)
renderWhereFromJSON(w, item.ti.Name, conn) renderWhereFromJSON(w, item.kvitem, "where", conn)
} else if conn, ok := item.data["_where"]; ok { } else if conn, ok := item.data["_where"]; ok {
io.WriteString(w, ` AND `) io.WriteString(w, ` AND `)
renderWhereFromJSON(w, item.ti.Name, conn) renderWhereFromJSON(w, item.kvitem, "_where", conn)
} }
} }
io.WriteString(w, `)`) io.WriteString(w, `)`)
@ -165,9 +168,28 @@ func renderNestedUpdateRelColumns(w io.Writer, item kvitem, values bool) error {
for _, v := range item.items { for _, v := range item.items {
if v._ctype > 0 && v.relCP.Type == RelOneToMany { if v._ctype > 0 && v.relCP.Type == RelOneToMany {
if values { if values {
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col) // if v.relCP.Right.Array {
// io.WriteString(w, `array_diff(`)
// colWithTable(w, v.relCP.Right.Table, v.relCP.Right.Col)
// io.WriteString(w, `, `)
// }
if v._ctype > 0 {
io.WriteString(w, `"_x_`)
io.WriteString(w, v.relCP.Left.Table)
io.WriteString(w, `".`)
quoted(w, v.relCP.Left.Col)
} else { } else {
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col)
}
// if v.relCP.Right.Array {
// io.WriteString(w, `)`)
// }
} else {
quoted(w, v.relCP.Right.Col) quoted(w, v.relCP.Right.Col)
} }
} }
} }
@ -176,12 +198,13 @@ func renderNestedUpdateRelColumns(w io.Writer, item kvitem, values bool) error {
} }
func renderNestedUpdateRelTables(w io.Writer, item kvitem) error { func renderNestedUpdateRelTables(w io.Writer, item kvitem) error {
// Render child foreign key columns if child-to-parent // Render tables needed to set values if child-to-parent
// relationship is one-to-many // relationship is one-to-many
for _, v := range item.items { for _, v := range item.items {
if v._ctype > 0 && v.relCP.Type == RelOneToMany { if v._ctype > 0 && v.relCP.Type == RelOneToMany {
quoted(w, v.relCP.Left.Table) io.WriteString(w, `"_x_`)
io.WriteString(w, `, `) io.WriteString(w, v.relCP.Left.Table)
io.WriteString(w, `", `)
} }
} }

View File

@ -7,26 +7,17 @@ import (
func singleUpdate(t *testing.T) { func singleUpdate(t *testing.T) {
gql := `mutation { gql := `mutation {
product(id: 15, update: $update, where: { id: { eq: 1 } }) { product(id: $id, update: $update, where: { id: { eq: 1 } }) {
id id
name name
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE ((("products"."id") IS NOT DISTINCT FROM 1) AND (("products"."id") = 15)) 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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), "update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "anon") compileGQLToPSQL(t, gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func simpleUpdateWithPresets(t *testing.T) { func simpleUpdateWithPresets(t *testing.T) {
@ -36,25 +27,16 @@ func simpleUpdateWithPresets(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE (("products"."user_id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint) 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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`), "data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") compileGQLToPSQL(t, gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func nestedUpdateManyToMany(t *testing.T) { func nestedUpdateManyToMany(t *testing.T) {
gql := `mutation { gql := `mutation {
purchase(update: $data, id: 5) { purchase(update: $data, id: $id) {
sale_type sale_type
quantity quantity
due_date due_date
@ -71,10 +53,6 @@ func nestedUpdateManyToMany(t *testing.T) {
} }
}` }`
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t)WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" 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 row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t)WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" 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 row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(` { "data": json.RawMessage(` {
"sale_type": "bought", "sale_type": "bought",
@ -92,17 +70,7 @@ func nestedUpdateManyToMany(t *testing.T) {
`), `),
} }
for i := 0; i < 1000; i++ { compileGQLToPSQL(t, gql, vars, "admin")
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql1 && string(resSQL) != sql2 {
t.Fatal(errNotExpected)
}
}
} }
func nestedUpdateOneToMany(t *testing.T) { func nestedUpdateOneToMany(t *testing.T) {
@ -119,8 +87,6 @@ func nestedUpdateOneToMany(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t)WHERE (("users"."id") IS NOT DISTINCT FROM 8) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id" = '2') RETURNING "products".*) 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"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"email": "thedude@rug.com", "email": "thedude@rug.com",
@ -139,19 +105,12 @@ func nestedUpdateOneToMany(t *testing.T) {
}`), }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "admin") compileGQLToPSQL(t, gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func nestedUpdateOneToOne(t *testing.T) { func nestedUpdateOneToOne(t *testing.T) {
gql := `mutation { gql := `mutation {
product(update: $data, id: 6) { product(update: $data, id: $id) {
id id
name name
user { user {
@ -162,8 +121,6 @@ func nestedUpdateOneToOne(t *testing.T) {
} }
}` }`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 6) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) 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", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"name": "Apple", "name": "Apple",
@ -176,19 +133,13 @@ func nestedUpdateOneToOne(t *testing.T) {
}`), }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "admin") compileGQLToPSQL(t, gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
} }
func nestedUpdateOneToManyWithConnect(t *testing.T) { func nestedUpdateOneToManyWithConnect(t *testing.T) {
gql := `mutation { gql := `mutation {
user(update: $data, id: 6) { user(update: $data, id: $id) {
id id
full_name full_name
email email
@ -200,8 +151,6 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) {
} }
}` }`
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t)WHERE (("users"."id") = 6) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id"FROM "users" WHERE ("products"."id" = '7') RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id" = '8') RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") 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"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"email": "thedude@rug.com", "email": "thedude@rug.com",
@ -215,19 +164,12 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) {
}`), }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "admin") compileGQLToPSQL(t, gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql1 {
t.Fatal(errNotExpected)
}
} }
func nestedUpdateOneToOneWithConnect(t *testing.T) { func nestedUpdateOneToOneWithConnect(t *testing.T) {
gql := `mutation { gql := `mutation {
product(update: $data, id: 9) { product(update: $data, id: $product_id) {
id id
name name
user { user {
@ -238,10 +180,6 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) {
} }
}` }`
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."id" = '5' AND "users"."email" = 'test@test.com' LIMIT 1), "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") = 9) 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", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."email" = 'test@test.com' AND "users"."id" = '5' LIMIT 1), "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") = 9) 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", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"name": "Apple", "name": "Apple",
@ -252,29 +190,17 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) {
}`), }`),
} }
for i := 0; i < 1000; i++ { compileGQLToPSQL(t, gql, vars, "admin")
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql1 && string(resSQL) != sql2 {
t.Fatal(errNotExpected)
}
}
} }
func nestedUpdateOneToOneWithDisconnect(t *testing.T) { func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
gql := `mutation { gql := `mutation {
product(update: $data, id: 2) { product(update: $data, id: $id) {
id id
name name
user_id user_id
} }
}` }`
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"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{ "data": json.RawMessage(`{
"name": "Apple", "name": "Apple",
@ -285,15 +211,39 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
}`), }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "admin") compileGQLToPSQL(t, gql, vars, "admin")
if err != nil {
t.Fatal(err)
} }
if string(resSQL) != sql { // func nestedUpdateOneToOneWithDisconnectArray(t *testing.T) {
t.Fatal(errNotExpected) // gql := `mutation {
} // product(update: $data, id: 2) {
} // id
// name
// user_id
// }
// }`
// 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"`
// vars := map[string]json.RawMessage{
// "data": json.RawMessage(`{
// "name": "Apple",
// "price": 1.25,
// "user": {
// "disconnect": { "id": 5 }
// }
// }`),
// }
// resSQL, err := compileGQLToPSQL(gql, vars, "admin")
// if err != nil {
// t.Fatal(err)
// }
// if string(resSQL) != sql {
// t.Fatal(errNotExpected)
// }
// }
func TestCompileUpdate(t *testing.T) { func TestCompileUpdate(t *testing.T) {
t.Run("singleUpdate", singleUpdate) t.Run("singleUpdate", singleUpdate)
@ -304,4 +254,5 @@ func TestCompileUpdate(t *testing.T) {
t.Run("nestedUpdateOneToManyWithConnect", nestedUpdateOneToManyWithConnect) t.Run("nestedUpdateOneToManyWithConnect", nestedUpdateOneToManyWithConnect)
t.Run("nestedUpdateOneToOneWithConnect", nestedUpdateOneToOneWithConnect) t.Run("nestedUpdateOneToOneWithConnect", nestedUpdateOneToOneWithConnect)
t.Run("nestedUpdateOneToOneWithDisconnect", nestedUpdateOneToOneWithDisconnect) t.Run("nestedUpdateOneToOneWithDisconnect", nestedUpdateOneToOneWithDisconnect)
//t.Run("nestedUpdateOneToOneWithDisconnectArray", nestedUpdateOneToOneWithDisconnectArray)
} }

View File

@ -45,6 +45,7 @@ type trval struct {
query struct { query struct {
limit string limit string
fil *Exp fil *Exp
filNU bool
cols map[string]struct{} cols map[string]struct{}
disable struct { disable struct {
funcs bool funcs bool
@ -53,6 +54,7 @@ type trval struct {
insert struct { insert struct {
fil *Exp fil *Exp
filNU bool
cols map[string]struct{} cols map[string]struct{}
psmap map[string]string psmap map[string]string
pslist []string pslist []string
@ -60,6 +62,7 @@ type trval struct {
update struct { update struct {
fil *Exp fil *Exp
filNU bool
cols map[string]struct{} cols map[string]struct{}
psmap map[string]string psmap map[string]string
pslist []string pslist []string
@ -67,6 +70,7 @@ type trval struct {
delete struct { delete struct {
fil *Exp fil *Exp
filNU bool
cols map[string]struct{} cols map[string]struct{}
} }
} }
@ -88,21 +92,21 @@ func (trv *trval) allowedColumns(qt QType) map[string]struct{} {
return nil return nil
} }
func (trv *trval) filter(qt QType) *Exp { func (trv *trval) filter(qt QType) (*Exp, bool) {
switch qt { switch qt {
case QTQuery: case QTQuery:
return trv.query.fil return trv.query.fil, trv.query.filNU
case QTInsert: case QTInsert:
return trv.insert.fil return trv.insert.fil, trv.insert.filNU
case QTUpdate: case QTUpdate:
return trv.update.fil return trv.update.fil, trv.update.filNU
case QTDelete: case QTDelete:
return trv.delete.fil return trv.delete.fil, trv.delete.filNU
case QTUpsert: case QTUpsert:
return trv.insert.fil return trv.insert.fil, trv.insert.filNU
} }
return nil return nil, false
} }
func listToMap(list []string) map[string]struct{} { func listToMap(list []string) map[string]struct{} {

View File

@ -4,7 +4,11 @@ package qcode
// FuzzerEntrypoint for Fuzzbuzz // FuzzerEntrypoint for Fuzzbuzz
func Fuzz(data []byte) int { func Fuzz(data []byte) int {
GetQType(string(data)) qt := GetQType(string(data))
if qt > QTUpsert {
panic("qt > QTUpsert")
}
qcompile, _ := NewCompiler(Config{}) qcompile, _ := NewCompiler(Config{})
_, err := qcompile.Compile(data, "user") _, err := qcompile.Compile(data, "user")

View File

@ -31,7 +31,7 @@ type item struct {
_type itemType // The type of this item. _type itemType // The type of this item.
pos Pos // The starting position, in bytes, of this item in the input string. pos Pos // The starting position, in bytes, of this item in the input string.
end Pos // The ending position, in bytes, of this item in the input string. end Pos // The ending position, in bytes, of this item in the input string.
line uint16 // The line number at the start of this item. line int16 // The line number at the start of this item.
} }
// itemType identifies the type of lex items. // itemType identifies the type of lex items.
@ -87,7 +87,7 @@ type lexer struct {
width Pos // width of last rune read from input width Pos // width of last rune read from input
items []item // array of scanned items items []item // array of scanned items
itemsA [50]item itemsA [50]item
line uint16 // 1+number of newlines seen line int16 // 1+number of newlines seen
err error err error
} }
@ -137,7 +137,7 @@ func (l *lexer) emit(t itemType) {
l.items = append(l.items, item{t, l.start, l.pos, l.line}) l.items = append(l.items, item{t, l.start, l.pos, l.line})
// Some items contain text internally. If so, count their newlines. // Some items contain text internally. If so, count their newlines.
switch t { switch t {
case itemName: case itemStringVal:
for i := l.start; i < l.pos; i++ { for i := l.start; i < l.pos; i++ {
if l.input[i] == '\n' { if l.input[i] == '\n' {
l.line++ l.line++
@ -155,11 +155,6 @@ func (l *lexer) emitL(t itemType) {
// ignore skips over the pending input before this point. // ignore skips over the pending input before this point.
func (l *lexer) ignore() { func (l *lexer) ignore() {
for i := l.start; i < l.pos; i++ {
if l.input[i] == '\n' {
l.line++
}
}
l.start = l.pos l.start = l.pos
} }
@ -436,7 +431,7 @@ func lowercase(b []byte, s Pos, e Pos) {
} }
} }
func (i *item) String() string { func (i item) String() string {
var v string var v string
switch i._type { switch i._type {

View File

@ -156,12 +156,19 @@ func parseSelectionSet(gql []byte) (*Operation, error) {
return nil, err return nil, err
} }
lexPool.Put(l) if p.peek(itemObjClose) {
p.ignore()
if err != nil { } else {
return nil, err return nil, fmt.Errorf("operation missing closing '}'")
} }
if !p.peek(itemEOF) {
p.ignore()
return nil, fmt.Errorf("invalid '%s' found after closing '}'", p.current())
}
lexPool.Put(l)
return op, err return op, err
} }
@ -184,11 +191,16 @@ func (p *Parser) ignore() {
p.pos = n p.pos = n
} }
func (p *Parser) current() string {
item := p.items[p.pos]
return b2s(p.input[item.pos:item.end])
}
func (p *Parser) peek(types ...itemType) bool { func (p *Parser) peek(types ...itemType) bool {
n := p.pos + 1 n := p.pos + 1
if p.items[n]._type == itemEOF { // if p.items[n]._type == itemEOF {
return false // return false
} // }
if n >= len(p.items) { if n >= len(p.items) {
return false return false
} }
@ -292,9 +304,10 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
if st.Len() == 0 { if st.Len() == 0 {
break break
} } else {
continue continue
} }
}
if !p.peek(itemName) { if !p.peek(itemName) {
return nil, errors.New("expecting an alias or field name") return nil, errors.New("expecting an alias or field name")
@ -306,6 +319,8 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
f.Args = f.argsA[:0] f.Args = f.argsA[:0]
f.Children = f.childrenA[:0] f.Children = f.childrenA[:0]
// Parse the inside of the the fields () parentheses
// in short parse the args like id, where, etc
if err := p.parseField(f); err != nil { if err := p.parseField(f); err != nil {
return nil, err return nil, err
} }
@ -318,6 +333,8 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
f.ParentID = -1 f.ParentID = -1
} }
// The first opening curley brackets after this
// comes the columns or child fields
if p.peek(itemObjOpen) { if p.peek(itemObjOpen) {
p.ignore() p.ignore()
st.Push(f.ID) st.Push(f.ID)
@ -539,6 +556,31 @@ func (t parserType) String() string {
return fmt.Sprintf("<%s>", v) return fmt.Sprintf("<%s>", v)
} }
func FreeNode(n *Node) { // type Frees struct {
// n *Node
// loc int
// }
// var freeList []Frees
// func FreeNode(n *Node, loc int) {
// j := -1
// for i := range freeList {
// if n == freeList[i].n {
// j = i
// break
// }
// }
// if j == -1 {
// nodePool.Put(n)
// freeList = append(freeList, Frees{n, loc})
// } else {
// fmt.Printf(">>>>(%d) RE_FREE %d %p %s %s\n", loc, freeList[j].loc, freeList[j].n, n.Name, n.Type)
// }
// }
func FreeNode(n *Node, loc int) {
nodePool.Put(n) nodePool.Put(n)
} }

View File

@ -17,13 +17,13 @@ func TestCompile1(t *testing.T) {
} }
_, err = qc.Compile([]byte(` _, err = qc.Compile([]byte(`
{ product(id: 15) { query { product(id: 15) {
id id
name name
} }`), "user") } }`), "user")
if err != nil { if err == nil {
t.Fatal(err) t.Fatal(errors.New("this should be an error id must be a variable"))
} }
} }
@ -39,7 +39,7 @@ func TestCompile2(t *testing.T) {
} }
_, err = qc.Compile([]byte(` _, err = qc.Compile([]byte(`
query { product(id: 15) { query { product(id: $id) {
id id
name name
} }`), "user") } }`), "user")
@ -62,7 +62,7 @@ func TestCompile3(t *testing.T) {
_, err = qc.Compile([]byte(` _, err = qc.Compile([]byte(`
mutation { mutation {
product(id: 15, name: "Test") { product(id: $test, name: "Test") {
id id
name name
} }
@ -100,6 +100,35 @@ func TestEmptyCompile(t *testing.T) {
} }
} }
func TestInvalidPostfixCompile(t *testing.T) {
gql := `mutation
updateThread {
thread(update: $data, where: { slug: { eq: $slug } }) {
slug
title
published
createdAt : created_at
totalVotes : cached_votes_total
totalPosts : cached_posts_total
vote : thread_vote(where: { user_id: { eq: $user_id } }) {
id
}
topics {
slug
name
}
}
}
}`
qcompile, _ := NewCompiler(Config{})
_, err := qcompile.Compile([]byte(gql), "anon")
if err == nil {
t.Fatal(errors.New("expecting an error"))
}
}
var gql = []byte(` var gql = []byte(`
products( products(
# returns only 30 items # returns only 30 items

View File

@ -51,6 +51,7 @@ type Select struct {
Allowed map[string]struct{} Allowed map[string]struct{}
PresetMap map[string]string PresetMap map[string]string
PresetList []string PresetList []string
SkipRender bool
} }
type Column struct { type Column struct {
@ -64,6 +65,7 @@ type Exp struct {
Col string Col string
NestedCols []string NestedCols []string
Type ValType Type ValType
Table string
Val string Val string
ListType ValType ListType ValType
ListVal []string ListVal []string
@ -83,9 +85,19 @@ type OrderBy struct {
Order Order Order Order
} }
type PagingType int
const (
PtOffset PagingType = iota
PtForward
PtBackward
)
type Paging struct { type Paging struct {
Type PagingType
Limit string Limit string
Offset string Offset string
Cursor bool
NoLimit bool NoLimit bool
} }
@ -119,6 +131,8 @@ const (
OpEqID OpEqID
OpTsQuery OpTsQuery
OpFalse OpFalse
OpNotDistinct
OpDistinct
) )
type ValType int type ValType int
@ -131,6 +145,7 @@ const (
ValList ValList
ValVar ValVar
ValNone ValNone
ValRef
) )
type AggregrateOp int type AggregrateOp int
@ -182,12 +197,19 @@ func NewCompiler(c Config) (*Compiler, error) {
return co, nil return co, nil
} }
func NewFilter() *Exp {
ex := expPool.Get().(*Exp)
ex.Reset()
return ex
}
func (com *Compiler) AddRole(role, table string, trc TRConfig) error { func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
var err error var err error
trv := &trval{} trv := &trval{}
// query config // query config
trv.query.fil, err = compileFilter(trc.Query.Filters) trv.query.fil, trv.query.filNU, err = compileFilter(trc.Query.Filters)
if err != nil { if err != nil {
return err return err
} }
@ -198,7 +220,8 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
trv.query.disable.funcs = trc.Query.DisableFunctions trv.query.disable.funcs = trc.Query.DisableFunctions
// insert config // insert config
if trv.insert.fil, err = compileFilter(trc.Insert.Filters); err != nil { trv.insert.fil, trv.insert.filNU, err = compileFilter(trc.Insert.Filters)
if err != nil {
return err return err
} }
trv.insert.cols = listToMap(trc.Insert.Columns) trv.insert.cols = listToMap(trc.Insert.Columns)
@ -206,7 +229,8 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
trv.insert.pslist = mapToList(trv.insert.psmap) trv.insert.pslist = mapToList(trv.insert.psmap)
// update config // update config
if trv.update.fil, err = compileFilter(trc.Update.Filters); err != nil { trv.update.fil, trv.update.filNU, err = compileFilter(trc.Update.Filters)
if err != nil {
return err return err
} }
trv.update.cols = listToMap(trc.Update.Columns) trv.update.cols = listToMap(trc.Update.Columns)
@ -214,7 +238,8 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
trv.update.pslist = mapToList(trv.update.psmap) trv.update.pslist = mapToList(trv.update.psmap)
// delete config // delete config
if trv.delete.fil, err = compileFilter(trc.Delete.Filters); err != nil { trv.delete.fil, trv.delete.filNU, err = compileFilter(trc.Delete.Filters)
if err != nil {
return err return err
} }
trv.delete.cols = listToMap(trc.Delete.Columns) trv.delete.cols = listToMap(trc.Delete.Columns)
@ -334,13 +359,13 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
s.FieldName = s.Name s.FieldName = s.Name
} }
err := com.compileArgs(qc, s, field.Args) err := com.compileArgs(qc, s, field.Args, role)
if err != nil { if err != nil {
return err return err
} }
// Order is important addFilters must come after compileArgs // Order is important AddFilters must come after compileArgs
com.addFilters(qc, s, role) com.AddFilters(qc, s, role)
if s.ParentID == -1 { if s.ParentID == -1 {
qc.Roots = append(qc.Roots, s.ID) qc.Roots = append(qc.Roots, s.ID)
@ -386,72 +411,82 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
return nil return nil
} }
func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) { func (com *Compiler) AddFilters(qc *QCode, sel *Select, role string) {
var fil *Exp var fil *Exp
var nu bool
if trv, ok := com.tr[role][sel.Name]; ok { if trv, ok := com.tr[role][sel.Name]; ok {
fil = trv.filter(qc.Type) fil, nu = trv.filter(qc.Type)
} else {
return } else if role == "anon" {
// Tables not defined under the anon role will not be rendered
sel.SkipRender = true
} }
if fil == nil { if fil == nil {
return return
} }
if nu && role == "anon" {
sel.SkipRender = true
}
switch fil.Op { switch fil.Op {
case OpNop: case OpNop:
case OpFalse: case OpFalse:
sel.Where = fil sel.Where = fil
default: default:
if sel.Where != nil { AddFilter(sel, fil)
ow := sel.Where
sel.Where = expPool.Get().(*Exp)
sel.Where.Reset()
sel.Where.Op = OpAnd
sel.Where.Children = sel.Where.childrenA[:2]
sel.Where.Children[0] = fil
sel.Where.Children[1] = ow
} else {
sel.Where = fil
}
} }
} }
func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg) error { func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg, role string) error {
var err error var err error
var ka bool
// don't free this arg either previously done or will be free'd
// in the future like in psql
var df bool
for i := range args { for i := range args {
arg := &args[i] arg := &args[i]
switch arg.Name { switch arg.Name {
case "id": case "id":
err, ka = com.compileArgID(sel, arg) err, df = com.compileArgID(sel, arg)
case "search": case "search":
err, ka = com.compileArgSearch(sel, arg) err, df = com.compileArgSearch(sel, arg)
case "where": case "where":
err, ka = com.compileArgWhere(sel, arg) err, df = com.compileArgWhere(sel, arg, role)
case "orderby", "order_by", "order": case "orderby", "order_by", "order":
err, ka = com.compileArgOrderBy(sel, arg) err, df = com.compileArgOrderBy(sel, arg)
case "distinct_on", "distinct": case "distinct_on", "distinct":
err, ka = com.compileArgDistinctOn(sel, arg) err, df = com.compileArgDistinctOn(sel, arg)
case "limit": case "limit":
err, ka = com.compileArgLimit(sel, arg) err, df = com.compileArgLimit(sel, arg)
case "offset": case "offset":
err, ka = com.compileArgOffset(sel, arg) err, df = com.compileArgOffset(sel, arg)
case "first":
err, df = com.compileArgFirstLast(sel, arg, PtForward)
case "last":
err, df = com.compileArgFirstLast(sel, arg, PtBackward)
case "after":
err, df = com.compileArgAfterBefore(sel, arg, PtForward)
case "before":
err, df = com.compileArgAfterBefore(sel, arg, PtBackward)
} }
if !ka { if !df {
nodePool.Put(arg.Val) FreeNode(arg.Val, 5)
} }
if err != nil { if err != nil {
@ -465,7 +500,7 @@ func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg) error {
func (com *Compiler) setMutationType(qc *QCode, args []Arg) error { func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
setActionVar := func(arg *Arg) error { setActionVar := func(arg *Arg) error {
if arg.Val.Type != NodeVar { if arg.Val.Type != NodeVar {
return fmt.Errorf("value for argument '%s' must be a variable", arg.Name) return argErr(arg.Name, "variable")
} }
qc.ActionVar = arg.Val.Val qc.ActionVar = arg.Val.Val
return nil return nil
@ -488,7 +523,7 @@ func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
qc.Type = QTDelete qc.Type = QTDelete
if arg.Val.Type != NodeBool { if arg.Val.Type != NodeBool {
return fmt.Errorf("value for argument '%s' must be a boolean", arg.Name) return argErr(arg.Name, "boolen")
} }
if arg.Val.Val == "false" { if arg.Val.Val == "false" {
@ -501,19 +536,20 @@ func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
return nil return nil
} }
func (com *Compiler) compileArgObj(st *util.Stack, arg *Arg) (*Exp, error) { func (com *Compiler) compileArgObj(st *util.Stack, arg *Arg) (*Exp, bool, error) {
if arg.Val.Type != NodeObj { if arg.Val.Type != NodeObj {
return nil, fmt.Errorf("expecting an object") return nil, false, fmt.Errorf("expecting an object")
} }
return com.compileArgNode(st, arg.Val, true) return com.compileArgNode(st, arg.Val, true)
} }
func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*Exp, error) { func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*Exp, bool, error) {
var root *Exp var root *Exp
var needsUser bool
if node == nil || len(node.Children) == 0 { if node == nil || len(node.Children) == 0 {
return nil, errors.New("invalid argument value") return nil, false, errors.New("invalid argument value")
} }
pushChild(st, nil, node) pushChild(st, nil, node)
@ -524,9 +560,10 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
} }
intf := st.Pop() intf := st.Pop()
node, ok := intf.(*Node) node, ok := intf.(*Node)
if !ok || node == nil { if !ok || node == nil {
return nil, fmt.Errorf("16: unexpected value %v (%t)", intf, intf) return nil, needsUser, fmt.Errorf("16: unexpected value %v (%t)", intf, intf)
} }
// Objects inside a list // Objects inside a list
@ -542,13 +579,17 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
ex, err := newExp(st, node, usePool) ex, err := newExp(st, node, usePool)
if err != nil { if err != nil {
return nil, err return nil, needsUser, err
} }
if ex == nil { if ex == nil {
continue continue
} }
if ex.Type == ValVar && ex.Val == "user_id" {
needsUser = true
}
if node.exp == nil { if node.exp == nil {
root = ex root = ex
} else { } else {
@ -556,22 +597,26 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
} }
} }
pushChild(st, nil, node) if usePool {
st.Push(node)
for { for {
if st.Len() == 0 { if st.Len() == 0 {
break break
} }
intf := st.Pop() intf := st.Pop()
node, _ := intf.(*Node) node, ok := intf.(*Node)
if !ok || node == nil {
continue
}
for i := range node.Children { for i := range node.Children {
st.Push(node.Children[i]) st.Push(node.Children[i])
} }
nodePool.Put(node) FreeNode(node, 1)
}
} }
return root, nil return root, needsUser, nil
} }
func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) { func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) {
@ -583,86 +628,58 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) {
return nil, false return nil, false
} }
if arg.Val.Type != NodeVar {
return argErr("id", "variable"), false
}
ex := expPool.Get().(*Exp) ex := expPool.Get().(*Exp)
ex.Reset() ex.Reset()
ex.Op = OpEqID ex.Op = OpEqID
ex.Val = arg.Val.Val
switch arg.Val.Type {
case NodeStr:
ex.Type = ValStr
case NodeInt:
ex.Type = ValInt
case NodeFloat:
ex.Type = ValFloat
case NodeVar:
ex.Type = ValVar ex.Type = ValVar
default: ex.Val = arg.Val.Val
return fmt.Errorf("expecting a string, int, float or variable"), false
}
sel.Where = ex sel.Where = ex
return nil, false return nil, false
} }
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) { func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) {
if arg.Val.Type != NodeVar {
return argErr("search", "variable"), false
}
ex := expPool.Get().(*Exp) ex := expPool.Get().(*Exp)
ex.Reset() ex.Reset()
ex.Op = OpTsQuery ex.Op = OpTsQuery
ex.Val = arg.Val.Val
if arg.Val.Type == NodeVar {
ex.Type = ValVar ex.Type = ValVar
} else { ex.Val = arg.Val.Val
ex.Type = ValStr
}
if sel.Args == nil { if sel.Args == nil {
sel.Args = make(map[string]*Node) sel.Args = make(map[string]*Node)
} }
sel.Args[arg.Name] = arg.Val sel.Args[arg.Name] = arg.Val
AddFilter(sel, ex)
if sel.Where != nil {
ow := sel.Where
sel.Where = expPool.Get().(*Exp)
sel.Where.Reset()
sel.Where.Op = OpAnd
sel.Where.Children = sel.Where.childrenA[:2]
sel.Where.Children[0] = ex
sel.Where.Children[1] = ow
} else {
sel.Where = ex
}
return nil, true return nil, true
} }
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg) (error, bool) { func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) (error, bool) {
st := util.NewStack() st := util.NewStack()
var err error var err error
ex, err := com.compileArgObj(st, arg) ex, nu, err := com.compileArgObj(st, arg)
if err != nil { if err != nil {
return err, false return err, false
} }
if sel.Where != nil { if nu && role == "anon" {
ow := sel.Where sel.SkipRender = true
sel.Where = expPool.Get().(*Exp)
sel.Where.Reset()
sel.Where.Op = OpAnd
sel.Where.Children = sel.Where.childrenA[:2]
sel.Where.Children[0] = ex
sel.Where.Children[1] = ow
} else {
sel.Where = ex
} }
AddFilter(sel, ex)
return nil, false return nil, true
} }
func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) { func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
@ -689,16 +706,12 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
} }
if _, ok := com.bl[node.Name]; ok { if _, ok := com.bl[node.Name]; ok {
nodePool.Put(node) FreeNode(node, 2)
continue continue
} }
if node.Type == NodeObj { if node.Type != NodeStr && node.Type != NodeVar {
for i := range node.Children { return fmt.Errorf("expecting a string or variable"), false
st.Push(node.Children[i])
}
nodePool.Put(node)
continue
} }
ob := &OrderBy{} ob := &OrderBy{}
@ -722,7 +735,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
setOrderByColName(ob, node) setOrderByColName(ob, node)
sel.OrderBy = append(sel.OrderBy, ob) sel.OrderBy = append(sel.OrderBy, ob)
nodePool.Put(node) FreeNode(node, 3)
} }
return nil, false return nil, false
} }
@ -744,7 +757,7 @@ func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) (error, bool) {
for i := range node.Children { for i := range node.Children {
sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val) sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val)
nodePool.Put(node.Children[i]) FreeNode(node.Children[i], 5)
} }
return nil, false return nil, false
@ -754,7 +767,7 @@ func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) (error, bool) {
node := arg.Val node := arg.Val
if node.Type != NodeInt { if node.Type != NodeInt {
return fmt.Errorf("expecting an integer"), false return argErr("limit", "number"), false
} }
sel.Paging.Limit = node.Val sel.Paging.Limit = node.Val
@ -765,14 +778,39 @@ func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) (error, bool) {
func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) { func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) {
node := arg.Val node := arg.Val
if node.Type != NodeInt { if node.Type != NodeVar {
return fmt.Errorf("expecting an integer"), false return argErr("offset", "variable"), false
} }
sel.Paging.Offset = node.Val sel.Paging.Offset = node.Val
return nil, false return nil, false
} }
func (com *Compiler) compileArgFirstLast(sel *Select, arg *Arg, pt PagingType) (error, bool) {
node := arg.Val
if node.Type != NodeInt {
return argErr(arg.Name, "number"), false
}
sel.Paging.Type = pt
sel.Paging.Limit = node.Val
return nil, false
}
func (com *Compiler) compileArgAfterBefore(sel *Select, arg *Arg, pt PagingType) (error, bool) {
node := arg.Val
if node.Type != NodeVar || node.Val != "cursor" {
return fmt.Errorf("value for argument '%s' must be a variable named $cursor", arg.Name), false
}
sel.Paging.Type = pt
sel.Paging.Cursor = true
return nil, false
}
var zeroTrv = &trval{} var zeroTrv = &trval{}
func (com *Compiler) getRole(role, field string) *trval { func (com *Compiler) getRole(role, field string) *trval {
@ -783,6 +821,27 @@ func (com *Compiler) getRole(role, field string) *trval {
} }
} }
func AddFilter(sel *Select, fil *Exp) {
if sel.Where != nil {
ow := sel.Where
if sel.Where.Op != OpAnd || !sel.Where.doFree {
sel.Where = expPool.Get().(*Exp)
sel.Where.Reset()
sel.Where.Op = OpAnd
sel.Where.Children = sel.Where.childrenA[:2]
sel.Where.Children[0] = fil
sel.Where.Children[1] = ow
} else {
sel.Where.Children = append(sel.Where.Children, fil)
}
} else {
sel.Where = fil
}
}
func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) { func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
name := node.Name name := node.Name
if name[0] == '_' { if name[0] == '_' {
@ -797,6 +856,7 @@ func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
} else { } else {
ex = &Exp{doFree: false} ex = &Exp{doFree: false}
} }
ex.Children = ex.childrenA[:0] ex.Children = ex.childrenA[:0]
switch name { switch name {
@ -878,6 +938,12 @@ func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
case "is_null": case "is_null":
ex.Op = OpIsNull ex.Op = OpIsNull
ex.Val = node.Val ex.Val = node.Val
case "null_eq", "ndis", "not_distinct":
ex.Op = OpNotDistinct
ex.Val = node.Val
case "null_neq", "dis", "distinct":
ex.Op = OpDistinct
ex.Val = node.Val
default: default:
pushChildren(st, node.exp, node) pushChildren(st, node.exp, node)
return nil, nil // skip node return nil, nil // skip node
@ -973,30 +1039,34 @@ func pushChildren(st *util.Stack, exp *Exp, node *Node) {
func pushChild(st *util.Stack, exp *Exp, node *Node) { func pushChild(st *util.Stack, exp *Exp, node *Node) {
node.Children[0].exp = exp node.Children[0].exp = exp
st.Push(node.Children[0]) st.Push(node.Children[0])
} }
func compileFilter(filter []string) (*Exp, error) { func compileFilter(filter []string) (*Exp, bool, error) {
var fl *Exp var fl *Exp
var needsUser bool
com := &Compiler{} com := &Compiler{}
st := util.NewStack() st := util.NewStack()
if len(filter) == 0 { if len(filter) == 0 {
return &Exp{Op: OpNop, doFree: false}, nil return &Exp{Op: OpNop, doFree: false}, false, nil
} }
for i := range filter { for i := range filter {
if filter[i] == "false" { if filter[i] == "false" {
return &Exp{Op: OpFalse, doFree: false}, nil return &Exp{Op: OpFalse, doFree: false}, false, nil
} }
node, err := ParseArgValue(filter[i]) node, err := ParseArgValue(filter[i])
if err != nil { if err != nil {
return nil, err return nil, false, err
} }
f, err := com.compileArgNode(st, node, false) f, nu, err := com.compileArgNode(st, node, false)
if err != nil { if err != nil {
return nil, err return nil, false, err
}
if nu {
needsUser = true
} }
// TODO: Invalid table names in nested where causes fail silently // TODO: Invalid table names in nested where causes fail silently
@ -1010,7 +1080,7 @@ func compileFilter(filter []string) (*Exp, error) {
fl = &Exp{Op: OpAnd, Children: []*Exp{fl, f}, doFree: false} fl = &Exp{Op: OpAnd, Children: []*Exp{fl, f}, doFree: false}
} }
} }
return fl, nil return fl, needsUser, nil
} }
func buildPath(a []string) string { func buildPath(a []string) string {
@ -1101,3 +1171,7 @@ func FreeExp(ex *Exp) {
expPool.Put(ex) expPool.Put(ex)
} }
} }
func argErr(name, ty string) error {
return fmt.Errorf("value for argument '%s' must be a %s", name, ty)
}

41
serv/actions.go Normal file
View File

@ -0,0 +1,41 @@
package serv
import (
"fmt"
"net/http"
)
type actionFn func(w http.ResponseWriter, r *http.Request) error
func newAction(a configAction) (http.Handler, error) {
var fn actionFn
var err error
if len(a.SQL) != 0 {
fn, err = newSQLAction(a)
} else {
return nil, fmt.Errorf("invalid config for action '%s'", a.Name)
}
if err != nil {
return nil, err
}
httpFn := func(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
errlog.Error().Err(err).Send()
errorResp(w, err)
}
}
return http.HandlerFunc(httpFn), nil
}
func newSQLAction(a configAction) (actionFn, error) {
fn := func(w http.ResponseWriter, r *http.Request) error {
_, err := db.Exec(r.Context(), a.SQL)
return err
}
return fn, nil
}

View File

@ -1,320 +0,0 @@
package serv
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"sort"
"strings"
)
const (
AL_QUERY int = iota + 1
AL_VARS
)
type allowItem struct {
name string
hash string
uri string
gql string
vars json.RawMessage
}
var _allowList allowList
type allowList struct {
list []*allowItem
index map[string]int
filepath string
saveChan chan *allowItem
active bool
}
func initAllowList(cpath string) {
_allowList = allowList{
index: make(map[string]int),
saveChan: make(chan *allowItem),
active: true,
}
if len(cpath) != 0 {
fp := path.Join(cpath, "allow.list")
if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp
} else if !os.IsNotExist(err) {
errlog.Fatal().Err(err).Send()
}
}
if len(_allowList.filepath) == 0 {
fp := "./allow.list"
if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp
} else if !os.IsNotExist(err) {
errlog.Fatal().Err(err).Send()
}
}
if len(_allowList.filepath) == 0 {
fp := "./config/allow.list"
if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp
} else if !os.IsNotExist(err) {
errlog.Fatal().Err(err).Send()
}
}
if len(_allowList.filepath) == 0 {
if conf.Production {
errlog.Fatal().Msg("allow.list not found")
}
if len(cpath) == 0 {
_allowList.filepath = "./config/allow.list"
} else {
_allowList.filepath = path.Join(cpath, "allow.list")
}
logger.Warn().Msg("allow.list not found")
} else {
_allowList.load()
}
go func() {
for v := range _allowList.saveChan {
_allowList.save(v)
}
}()
}
func (al *allowList) add(req *gqlReq) {
if al.saveChan == nil || len(req.ref) == 0 || len(req.Query) == 0 {
return
}
var query string
for i := 0; i < len(req.Query); i++ {
c := req.Query[i]
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' {
query = req.Query
break
} else if c == '{' {
query = "query " + req.Query
break
}
}
al.saveChan <- &allowItem{
uri: req.ref,
gql: query,
vars: req.Vars,
}
}
func (al *allowList) upsert(query, vars []byte, uri string) {
q := string(query)
hash := gqlHash(q, vars, "")
name := gqlName(q)
var key string
if len(name) == 0 {
key = hash
} else {
key = name
}
if i, ok := al.index[key]; !ok {
al.list = append(al.list, &allowItem{
name: name,
hash: hash,
uri: uri,
gql: q,
vars: vars,
})
al.index[key] = len(al.list) - 1
} else {
item := al.list[i]
item.name = name
item.hash = hash
item.gql = q
item.vars = vars
}
}
func (al *allowList) load() {
b, err := ioutil.ReadFile(al.filepath)
if err != nil {
log.Fatal(err)
}
if len(b) == 0 {
return
}
var uri string
var varBytes []byte
s, e, c := 0, 0, 0
ty := 0
for {
if c == 0 && b[e] == '#' {
s = e
for e < len(b) && b[e] != '\n' {
e++
}
if (e - s) > 2 {
uri = strings.TrimSpace(string(b[(s + 1):e]))
}
}
if e >= len(b) {
break
}
if matchPrefix(b, e, "query") || matchPrefix(b, e, "mutation") {
if c == 0 {
s = e
}
ty = AL_QUERY
} else if matchPrefix(b, e, "variables") {
if c == 0 {
s = e + len("variables") + 1
}
ty = AL_VARS
} else if b[e] == '{' {
c++
} else if b[e] == '}' {
c--
if c == 0 {
if ty == AL_QUERY {
al.upsert(b[s:(e+1)], varBytes, uri)
varBytes = nil
} else if ty == AL_VARS {
varBytes = b[s:(e + 1)]
}
ty = 0
}
}
e++
if e >= len(b) {
break
}
}
}
func (al *allowList) save(item *allowItem) {
var err error
item.hash = gqlHash(item.gql, item.vars, "")
item.name = gqlName(item.gql)
if len(item.name) == 0 {
key := item.hash
if _, ok := al.index[key]; ok {
return
}
al.list = append(al.list, item)
al.index[key] = len(al.list) - 1
} else {
key := item.name
if i, ok := al.index[key]; ok {
if al.list[i].hash == item.hash {
return
}
al.list[i] = item
} else {
al.list = append(al.list, item)
al.index[key] = len(al.list) - 1
}
}
f, err := os.Create(al.filepath)
if err != nil {
logger.Warn().Err(err).Msgf("Failed to write allow list: %s", al.filepath)
return
}
defer f.Close()
keys := []string{}
urlMap := make(map[string][]*allowItem)
for _, v := range al.list {
urlMap[v.uri] = append(urlMap[v.uri], v)
}
for k := range urlMap {
keys = append(keys, k)
}
sort.Strings(keys)
for i := range keys {
k := keys[i]
v := urlMap[k]
if _, err := f.WriteString(fmt.Sprintf("# %s\n\n", k)); err != nil {
logger.Error().Err(err).Send()
return
}
for i := range v {
if len(v[i].vars) != 0 && !bytes.Equal(v[i].vars, []byte("{}")) {
vj, err := json.MarshalIndent(v[i].vars, "", "\t")
if err != nil {
logger.Warn().Err(err).Msg("Failed to write allow list 'vars' to file")
continue
}
_, err = f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
if err != nil {
logger.Error().Err(err).Send()
return
}
}
if v[i].gql[0] == '{' {
_, err = f.WriteString(fmt.Sprintf("query %s\n\n", v[i].gql))
} else {
_, err = f.WriteString(fmt.Sprintf("%s\n\n", v[i].gql))
}
if err != nil {
logger.Error().Err(err).Send()
return
}
}
}
}
func matchPrefix(b []byte, i int, s string) bool {
if (len(b) - i) < len(s) {
return false
}
for n := 0; n < len(s); n++ {
if b[(i+n)] != s[n] {
return false
}
}
return true
}

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
@ -18,31 +17,41 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
if v := ctx.Value(userIDProviderKey); v != nil { if v := ctx.Value(userIDProviderKey); v != nil {
return io.WriteString(w, v.(string)) return io.WriteString(w, v.(string))
} }
return 0, errors.New("query requires variable $user_id_provider") return 0, argErr("user_id_provider")
case "user_id": case "user_id":
if v := ctx.Value(userIDKey); v != nil { if v := ctx.Value(userIDKey); v != nil {
return io.WriteString(w, v.(string)) return io.WriteString(w, v.(string))
} }
return 0, errors.New("query requires variable $user_id") return 0, argErr("user_id")
case "user_role": case "user_role":
if v := ctx.Value(userRoleKey); v != nil { if v := ctx.Value(userRoleKey); v != nil {
return io.WriteString(w, v.(string)) return io.WriteString(w, v.(string))
} }
return 0, errors.New("query requires variable $user_role") return 0, argErr("user_role")
} }
fields := jsn.Get(vars, [][]byte{[]byte(tag)}) fields := jsn.Get(vars, [][]byte{[]byte(tag)})
if len(fields) == 0 { if len(fields) == 0 {
return 0, nil return 0, argErr(tag)
} }
v := fields[0].Value v := fields[0].Value
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
fields[0].Value = v[1 : len(v)-1] fields[0].Value = v[1 : len(v)-1]
} }
if tag == "cursor" {
v1, err := decrypt(string(fields[0].Value))
if err != nil {
return 0, err
}
return w.Write(v1)
}
return w.Write(escQuote(fields[0].Value)) return w.Write(escQuote(fields[0].Value))
} }
} }
@ -63,27 +72,37 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
for i := range args { for i := range args {
av := args[i] av := args[i]
switch { switch {
case bytes.Equal(av, []byte("user_id")): case bytes.Equal(av, []byte("user_id")):
if v := ctx.Value(userIDKey); v != nil { if v := ctx.Value(userIDKey); v != nil {
vars[i] = v.(string) vars[i] = v.(string)
} else { } else {
return nil, errors.New("query requires variable $user_id") return nil, argErr("user_id")
} }
case bytes.Equal(av, []byte("user_id_provider")): case bytes.Equal(av, []byte("user_id_provider")):
if v := ctx.Value(userIDProviderKey); v != nil { if v := ctx.Value(userIDProviderKey); v != nil {
vars[i] = v.(string) vars[i] = v.(string)
} else { } else {
return nil, errors.New("query requires variable $user_id_provider") return nil, argErr("user_id_provider")
} }
case bytes.Equal(av, []byte("user_role")): case bytes.Equal(av, []byte("user_role")):
if v := ctx.Value(userRoleKey); v != nil { if v := ctx.Value(userRoleKey); v != nil {
vars[i] = v.(string) vars[i] = v.(string)
} else { } else {
return nil, errors.New("query requires variable $user_role") return nil, argErr("user_role")
}
case bytes.Equal(av, []byte("cursor")):
if v, ok := fields["cursor"]; ok && v[0] == '"' {
v1, err := decrypt(string(v[1 : len(v)-1]))
if err != nil {
return nil, err
}
vars[i] = v1
} else {
return nil, argErr("cursor")
} }
default: default:
@ -96,11 +115,12 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
if err := json.Unmarshal(v, &val); err != nil { if err := json.Unmarshal(v, &val); err != nil {
return nil, err return nil, err
} }
vars[i] = val vars[i] = val
} }
} else { } else {
return nil, fmt.Errorf("query requires variable $%s", string(av)) return nil, argErr(string(av))
} }
} }
} }
@ -135,3 +155,7 @@ func escQuote(b []byte) []byte {
} }
return buf.Bytes() return buf.Bytes()
} }
func argErr(name string) error {
return fmt.Errorf("query requires variable '%s' to be set", name)
}

View File

@ -3,7 +3,6 @@ package serv
import ( import (
"context" "context"
"net/http" "net/http"
"strings"
) )
type ctxkey int type ctxkey int
@ -14,7 +13,7 @@ const (
userRoleKey userRoleKey
) )
func headerAuth(next http.Handler) http.HandlerFunc { func headerAuth(authc configAuth, next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -37,28 +36,53 @@ func headerAuth(next http.Handler) http.HandlerFunc {
} }
} }
func withAuth(next http.Handler) http.Handler { func headerHandler(authc configAuth, next http.Handler) http.HandlerFunc {
at := conf.Auth.Type hdr := authc.Header
ru := conf.Auth.Rails.URL
if conf.Auth.CredsInHeader { if len(hdr.Name) == 0 {
next = headerAuth(next) errlog.Fatal().Str("auth", authc.Name).Msg("no header.name defined")
} }
switch at { if !hdr.Exists && len(hdr.Value) == 0 {
errlog.Fatal().Str("auth", authc.Name).Msg("no header.value defined")
}
return func(w http.ResponseWriter, r *http.Request) {
var fo1 bool
value := r.Header.Get(hdr.Name)
switch {
case hdr.Exists:
fo1 = (len(value) == 0)
default:
fo1 = (value != hdr.Value)
}
if fo1 {
http.Error(w, "401 unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
}
}
func withAuth(next http.Handler, authc configAuth) http.Handler {
if authc.CredsInHeader {
next = headerAuth(authc, next)
}
switch authc.Type {
case "rails": case "rails":
if strings.HasPrefix(ru, "memcache:") { return railsHandler(authc, next)
return railsMemcacheHandler(next)
}
if strings.HasPrefix(ru, "redis:") {
return railsRedisHandler(next)
}
return railsCookieHandler(next)
case "jwt": case "jwt":
return jwtHandler(next) return jwtHandler(authc, next)
case "header":
return headerHandler(authc, next)
} }
return next return next

View File

@ -14,18 +14,18 @@ const (
jwtAuth0 int = iota + 1 jwtAuth0 int = iota + 1
) )
func jwtHandler(next http.Handler) http.HandlerFunc { func jwtHandler(authc configAuth, next http.Handler) http.HandlerFunc {
var key interface{} var key interface{}
var jwtProvider int var jwtProvider int
cookie := conf.Auth.Cookie cookie := authc.Cookie
if conf.Auth.JWT.Provider == "auth0" { if authc.JWT.Provider == "auth0" {
jwtProvider = jwtAuth0 jwtProvider = jwtAuth0
} }
secret := conf.Auth.JWT.Secret secret := authc.JWT.Secret
publicKeyFile := conf.Auth.JWT.PubKeyFile publicKeyFile := authc.JWT.PubKeyFile
switch { switch {
case len(secret) != 0: case len(secret) != 0:
@ -37,7 +37,7 @@ func jwtHandler(next http.Handler) http.HandlerFunc {
errlog.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
switch conf.Auth.JWT.PubKeyType { switch authc.JWT.PubKeyType {
case "ecdsa": case "ecdsa":
key, err = jwt.ParseECPublicKeyFromPEM(kd) key, err = jwt.ParseECPublicKeyFromPEM(kd)

View File

@ -6,32 +6,47 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/bradfitz/gomemcache/memcache" "github.com/bradfitz/gomemcache/memcache"
"github.com/dosco/super-graph/rails" "github.com/dosco/super-graph/rails"
"github.com/garyburd/redigo/redis" "github.com/garyburd/redigo/redis"
) )
func railsRedisHandler(next http.Handler) http.HandlerFunc { func railsHandler(authc configAuth, next http.Handler) http.HandlerFunc {
cookie := conf.Auth.Cookie ru := authc.Rails.URL
if strings.HasPrefix(ru, "memcache:") {
return railsMemcacheHandler(authc, next)
}
if strings.HasPrefix(ru, "redis:") {
return railsRedisHandler(authc, next)
}
return railsCookieHandler(authc, next)
}
func railsRedisHandler(authc configAuth, next http.Handler) http.HandlerFunc {
cookie := authc.Cookie
if len(cookie) == 0 { if len(cookie) == 0 {
errlog.Fatal().Msg("no auth.cookie defined") errlog.Fatal().Msg("no auth.cookie defined")
} }
if len(conf.Auth.Rails.URL) == 0 { if len(authc.Rails.URL) == 0 {
errlog.Fatal().Msg("no auth.rails.url defined") errlog.Fatal().Msg("no auth.rails.url defined")
} }
rp := &redis.Pool{ rp := &redis.Pool{
MaxIdle: conf.Auth.Rails.MaxIdle, MaxIdle: authc.Rails.MaxIdle,
MaxActive: conf.Auth.Rails.MaxActive, MaxActive: authc.Rails.MaxActive,
Dial: func() (redis.Conn, error) { Dial: func() (redis.Conn, error) {
c, err := redis.DialURL(conf.Auth.Rails.URL) c, err := redis.DialURL(authc.Rails.URL)
if err != nil { if err != nil {
errlog.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
pwd := conf.Auth.Rails.Password pwd := authc.Rails.Password
if len(pwd) != 0 { if len(pwd) != 0 {
if _, err := c.Do("AUTH", pwd); err != nil { if _, err := c.Do("AUTH", pwd); err != nil {
errlog.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
@ -66,17 +81,17 @@ func railsRedisHandler(next http.Handler) http.HandlerFunc {
} }
} }
func railsMemcacheHandler(next http.Handler) http.HandlerFunc { func railsMemcacheHandler(authc configAuth, next http.Handler) http.HandlerFunc {
cookie := conf.Auth.Cookie cookie := authc.Cookie
if len(cookie) == 0 { if len(cookie) == 0 {
errlog.Fatal().Msg("no auth.cookie defined") errlog.Fatal().Msg("no auth.cookie defined")
} }
if len(conf.Auth.Rails.URL) == 0 { if len(authc.Rails.URL) == 0 {
errlog.Fatal().Msg("no auth.rails.url defined") errlog.Fatal().Msg("no auth.rails.url defined")
} }
rURL, err := url.Parse(conf.Auth.Rails.URL) rURL, err := url.Parse(authc.Rails.URL)
if err != nil { if err != nil {
errlog.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
@ -108,13 +123,13 @@ func railsMemcacheHandler(next http.Handler) http.HandlerFunc {
} }
} }
func railsCookieHandler(next http.Handler) http.HandlerFunc { func railsCookieHandler(authc configAuth, next http.Handler) http.HandlerFunc {
cookie := conf.Auth.Cookie cookie := authc.Cookie
if len(cookie) == 0 { if len(cookie) == 0 {
errlog.Fatal().Msg("no auth.cookie defined") errlog.Fatal().Msg("no auth.cookie defined")
} }
ra, err := railsAuth(conf) ra, err := railsAuth(authc)
if err != nil { if err != nil {
errlog.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
@ -139,13 +154,13 @@ func railsCookieHandler(next http.Handler) http.HandlerFunc {
} }
} }
func railsAuth(c *config) (*rails.Auth, error) { func railsAuth(authc configAuth) (*rails.Auth, error) {
secret := c.Auth.Rails.SecretKeyBase secret := authc.Rails.SecretKeyBase
if len(secret) == 0 { if len(secret) == 0 {
return nil, errors.New("no auth.rails.secret_key_base defined") return nil, errors.New("no auth.rails.secret_key_base defined")
} }
version := c.Auth.Rails.Version version := authc.Rails.Version
if len(version) == 0 { if len(version) == 0 {
return nil, errors.New("no auth.rails.version defined") return nil, errors.New("no auth.rails.version defined")
} }
@ -155,16 +170,16 @@ func railsAuth(c *config) (*rails.Auth, error) {
return nil, err return nil, err
} }
if len(c.Auth.Rails.Salt) != 0 { if len(authc.Rails.Salt) != 0 {
ra.Salt = c.Auth.Rails.Salt ra.Salt = authc.Rails.Salt
} }
if len(conf.Auth.Rails.SignSalt) != 0 { if len(authc.Rails.SignSalt) != 0 {
ra.SignSalt = c.Auth.Rails.SignSalt ra.SignSalt = authc.Rails.SignSalt
} }
if len(conf.Auth.Rails.AuthSalt) != 0 { if len(authc.Rails.AuthSalt) != 0 {
ra.AuthSalt = c.Auth.Rails.AuthSalt ra.AuthSalt = authc.Rails.AuthSalt
} }
return ra, nil return ra, nil

View File

@ -1,15 +1,13 @@
package serv package serv
import ( import (
"context"
"fmt" "fmt"
"os"
"runtime" "runtime"
"strings" "strings"
"github.com/dosco/super-graph/allow"
"github.com/dosco/super-graph/psql" "github.com/dosco/super-graph/psql"
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool" "github.com/jackc/pgx/v4/pgxpool"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -37,11 +35,14 @@ var (
confPath string // path to the config file confPath string // path to the config file
db *pgxpool.Pool // database connection pool db *pgxpool.Pool // database connection pool
schema *psql.DBSchema // database tables, columns and relationships schema *psql.DBSchema // database tables, columns and relationships
allowList *allow.List // allow.list is contains queries allowed in production
qcompile *qcode.Compiler // qcode compiler qcompile *qcode.Compiler // qcode compiler
pcompile *psql.Compiler // postgres sql compiler pcompile *psql.Compiler // postgres sql compiler
secretKey [32]byte // encryption key
internalKey [32]byte // encryption key used for internal needs
) )
func Init() { func Cmd() {
initLog() initLog()
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
@ -156,159 +157,6 @@ e.g. db:migrate -+1
} }
} }
func initLog() {
out := zerolog.ConsoleWriter{Out: os.Stderr}
logger = zerolog.New(out).With().Timestamp().Logger()
errlog = logger.With().Caller().Logger()
}
func initConf() (*config, error) {
vi := newConfig(getConfigName())
if err := vi.ReadInConfig(); err != nil {
return nil, err
}
inherits := vi.GetString("inherits")
if len(inherits) != 0 {
vi = newConfig(inherits)
if err := vi.ReadInConfig(); err != nil {
return nil, err
}
if vi.IsSet("inherits") {
errlog.Fatal().Msgf("inherited config (%s) cannot itself inherit (%s)",
inherits,
vi.GetString("inherits"))
}
vi.SetConfigName(getConfigName())
if err := vi.MergeInConfig(); err != nil {
return nil, err
}
}
c := &config{}
if err := c.Init(vi); err != nil {
return nil, fmt.Errorf("unable to decode config, %v", err)
}
logLevel, err := zerolog.ParseLevel(c.LogLevel)
if err != nil {
errlog.Error().Err(err).Msg("error setting log_level")
}
zerolog.SetGlobalLevel(logLevel)
return c, nil
}
func initDB(c *config, useDB bool) (*pgx.Conn, error) {
config, _ := pgx.ParseConfig("")
config.Host = c.DB.Host
config.Port = c.DB.Port
config.User = c.DB.User
config.Password = c.DB.Password
config.RuntimeParams = map[string]string{
"application_name": c.AppName,
"search_path": c.DB.Schema,
}
if useDB {
config.Database = c.DB.DBName
}
switch c.LogLevel {
case "debug":
config.LogLevel = pgx.LogLevelDebug
case "info":
config.LogLevel = pgx.LogLevelInfo
case "warn":
config.LogLevel = pgx.LogLevelWarn
case "error":
config.LogLevel = pgx.LogLevelError
default:
config.LogLevel = pgx.LogLevelNone
}
config.Logger = NewSQLLogger(logger)
db, err := pgx.ConnectConfig(context.Background(), config)
if err != nil {
return nil, err
}
return db, nil
}
func initDBPool(c *config) (*pgxpool.Pool, error) {
config, _ := pgxpool.ParseConfig("")
config.ConnConfig.Host = c.DB.Host
config.ConnConfig.Port = c.DB.Port
config.ConnConfig.Database = c.DB.DBName
config.ConnConfig.User = c.DB.User
config.ConnConfig.Password = c.DB.Password
config.ConnConfig.RuntimeParams = map[string]string{
"application_name": c.AppName,
"search_path": c.DB.Schema,
}
switch c.LogLevel {
case "debug":
config.ConnConfig.LogLevel = pgx.LogLevelDebug
case "info":
config.ConnConfig.LogLevel = pgx.LogLevelInfo
case "warn":
config.ConnConfig.LogLevel = pgx.LogLevelWarn
case "error":
config.ConnConfig.LogLevel = pgx.LogLevelError
default:
config.ConnConfig.LogLevel = pgx.LogLevelNone
}
config.ConnConfig.Logger = NewSQLLogger(logger)
// if c.DB.MaxRetries != 0 {
// opt.MaxRetries = c.DB.MaxRetries
// }
if c.DB.PoolSize != 0 {
config.MaxConns = conf.DB.PoolSize
}
db, err := pgxpool.ConnectConfig(context.Background(), config)
if err != nil {
return nil, err
}
return db, nil
}
func initCompiler() {
var err error
qcompile, pcompile, err = initCompilers(conf)
if err != nil {
errlog.Fatal().Err(err).Msg("failed to initialize compilers")
}
if err := initResolvers(); err != nil {
errlog.Fatal().Err(err).Msg("failed to initialized resolvers")
}
}
func initConfOnce() {
var err error
if conf == nil {
if conf, err = initConf(); err != nil {
errlog.Fatal().Err(err).Msg("failed to read config")
}
}
}
func cmdVersion(cmd *cobra.Command, args []string) { func cmdVersion(cmd *cobra.Command, args []string) {
fmt.Printf("%s\n", BuildDetails()) fmt.Printf("%s\n", BuildDetails())
} }
@ -324,7 +172,7 @@ Branch : %v
Go version : %v Go version : %v
Licensed under the Apache Public License 2.0 Licensed under the Apache Public License 2.0
Copyright 2015-2019 Vikram Rangnekar. Copyright 2020, Vikram Rangnekar.
`, `,
version, version,
lastCommitSHA, lastCommitSHA,

View File

@ -63,7 +63,7 @@ func cmdDBCreate(cmd *cobra.Command, args []string) {
} }
defer conn.Close(ctx) defer conn.Close(ctx)
sql := fmt.Sprintf("CREATE DATABASE %s", conf.DB.DBName) sql := fmt.Sprintf(`CREATE DATABASE "%s"`, conf.DB.DBName)
_, err = conn.Exec(ctx, sql) _, err = conn.Exec(ctx, sql)
if err != nil { if err != nil {
@ -83,7 +83,7 @@ func cmdDBDrop(cmd *cobra.Command, args []string) {
} }
defer conn.Close(ctx) defer conn.Close(ctx)
sql := fmt.Sprintf(`DROP DATABASE IF EXISTS %s`, conf.DB.DBName) sql := fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, conf.DB.DBName)
_, err = conn.Exec(ctx, sql) _, err = conn.Exec(ctx, sql)
if err != nil { if err != nil {
@ -311,3 +311,13 @@ func getMigrationVars() map[string]interface{} {
"env": strings.ToLower(os.Getenv("GO_ENV")), "env": strings.ToLower(os.Getenv("GO_ENV")),
} }
} }
func initConfOnce() {
var err error
if conf == nil {
if conf, err = initConf(); err != nil {
errlog.Fatal().Err(err).Msg("failed to read config")
}
}
}

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math/rand"
"os" "os"
"path" "path"
@ -145,6 +146,17 @@ func logFunc(args ...interface{}) {
} }
} }
func avatarURL(size int) string {
if size == 0 {
size = 200
}
return fmt.Sprintf("https://i.pravatar.cc/%d?%d", size, rand.Intn(5000))
}
func imageURL(width int, height int) string {
return fmt.Sprintf("https://picsum.photos/%d/%d?%d", width, height, rand.Intn(5000))
}
//nolint: errcheck //nolint: errcheck
func setFakeFuncs(f *goja.Object) { func setFakeFuncs(f *goja.Object) {
gofakeit.Seed(0) gofakeit.Seed(0)
@ -222,7 +234,8 @@ func setFakeFuncs(f *goja.Object) {
// Internet // Internet
f.Set("url", gofakeit.URL) f.Set("url", gofakeit.URL)
f.Set("image_url", gofakeit.ImageURL) f.Set("image_url", imageURL)
f.Set("avatar_url", avatarURL)
f.Set("domain_name", gofakeit.DomainName) f.Set("domain_name", gofakeit.DomainName)
f.Set("domain_suffix", gofakeit.DomainSuffix) f.Set("domain_suffix", gofakeit.DomainSuffix)
f.Set("ipv4_address", gofakeit.IPv4Address) f.Set("ipv4_address", gofakeit.IPv4Address)

View File

@ -7,19 +7,25 @@ import (
func cmdServ(cmd *cobra.Command, args []string) { func cmdServ(cmd *cobra.Command, args []string) {
var err error var err error
initWatcher(confPath)
if conf, err = initConf(); err != nil { if conf, err = initConf(); err != nil {
errlog.Fatal().Err(err).Msg("failed to read config") fatalInProd(err, "failed to read config")
} }
db, err = initDBPool(conf) db, err = initDBPool(conf)
if err != nil { if err != nil {
errlog.Fatal().Err(err).Msg("failed to connect to database") fatalInProd(err, "failed to connect to database")
} }
if conf != nil && db != nil {
initCrypto()
initCompiler() initCompiler()
initResolvers()
initAllowList(confPath) initAllowList(confPath)
initPreparedList() initPreparedList(confPath)
initWatcher(confPath) }
startHTTP() startHTTP()
} }

View File

@ -30,10 +30,44 @@ type config struct {
AuthFailBlock bool `mapstructure:"auth_fail_block"` AuthFailBlock bool `mapstructure:"auth_fail_block"`
SeedFile string `mapstructure:"seed_file"` SeedFile string `mapstructure:"seed_file"`
MigrationsPath string `mapstructure:"migrations_path"` MigrationsPath string `mapstructure:"migrations_path"`
SecretKey string `mapstructure:"secret_key"`
Inflections map[string]string Inflections map[string]string
Auth struct { Auth configAuth
Auths []configAuth
DB struct {
Type string
Host string
Port uint16
DBName string
User string
Password string
Schema string
PoolSize int32 `mapstructure:"pool_size"`
MaxRetries int `mapstructure:"max_retries"`
SetUserID bool `mapstructure:"set_user_id"`
PingTimeout time.Duration `mapstructure:"ping_timeout"`
Vars map[string]string `mapstructure:"variables"`
Blocklist []string
Tables []configTable
} `mapstructure:"database"`
Actions []configAction
Tables []configTable
RolesQuery string `mapstructure:"roles_query"`
Roles []configRole
roles map[string]*configRole
abacEnabled bool
}
type configAuth struct {
Name string
Type string Type string
Cookie string Cookie string
CredsInHeader bool `mapstructure:"creds_in_header"` CredsInHeader bool `mapstructure:"creds_in_header"`
@ -56,37 +90,17 @@ type config struct {
PubKeyFile string `mapstructure:"public_key_file"` PubKeyFile string `mapstructure:"public_key_file"`
PubKeyType string `mapstructure:"public_key_type"` PubKeyType string `mapstructure:"public_key_type"`
} }
Header struct {
Name string
Value string
Exists bool
} }
DB struct {
Type string
Host string
Port uint16
DBName string
User string
Password string
Schema string
PoolSize int32 `mapstructure:"pool_size"`
MaxRetries int `mapstructure:"max_retries"`
SetUserID bool `mapstructure:"set_user_id"`
PingTimeout time.Duration `mapstructure:"ping_timeout"`
Vars map[string]string `mapstructure:"variables"`
Blocklist []string
Tables []configTable
} `mapstructure:"database"`
Tables []configTable
RolesQuery string `mapstructure:"roles_query"`
Roles []configRole
roles map[string]*configRole
abacEnabled bool
} }
type configColumn struct { type configColumn struct {
Name string Name string
Type string
ForeignKey string `mapstructure:"related_to"` ForeignKey string `mapstructure:"related_to"`
} }
@ -155,6 +169,12 @@ type configRole struct {
tablesMap map[string]*configRoleTable tablesMap map[string]*configRoleTable
} }
type configAction struct {
Name string
SQL string
AuthName string `mapstructure:"auth_name"`
}
func newConfig(name string) *viper.Viper { func newConfig(name string) *viper.Viper {
vi := viper.New() vi := viper.New()
@ -282,26 +302,48 @@ func (c *config) Init(vi *viper.Viper) error {
func (c *config) validate() { func (c *config) validate() {
rm := make(map[string]struct{}) rm := make(map[string]struct{})
for i := range c.Roles { for _, v := range c.Roles {
name := c.Roles[i].Name name := strings.ToLower(v.Name)
if _, ok := rm[name]; ok { if _, ok := rm[name]; ok {
errlog.Fatal().Msgf("duplicate config for role '%s'", c.Roles[i].Name) errlog.Fatal().Msgf("duplicate config for role '%s'", v.Name)
} }
rm[name] = struct{}{} rm[name] = struct{}{}
} }
tm := make(map[string]struct{}) tm := make(map[string]struct{})
for i := range c.Tables { for _, v := range c.Tables {
name := c.Tables[i].Name name := strings.ToLower(v.Name)
if _, ok := tm[name]; ok { if _, ok := tm[name]; ok {
errlog.Fatal().Msgf("duplicate config for table '%s'", c.Tables[i].Name) errlog.Fatal().Msgf("duplicate config for table '%s'", v.Name)
} }
tm[name] = struct{}{} tm[name] = struct{}{}
} }
am := make(map[string]struct{})
for _, v := range c.Auths {
name := strings.ToLower(v.Name)
if _, ok := am[name]; ok {
errlog.Fatal().Msgf("duplicate config for auth '%s'", v.Name)
}
am[name] = struct{}{}
}
for _, v := range c.Actions {
if len(v.AuthName) == 0 {
continue
}
authName := strings.ToLower(v.AuthName)
if _, ok := am[authName]; !ok {
errlog.Fatal().Msgf("invalid auth_name for action '%s'", v.Name)
}
}
if len(c.RolesQuery) == 0 { if len(c.RolesQuery) == 0 {
logger.Warn().Msgf("no 'roles_query' defined.") logger.Warn().Msgf("no 'roles_query' defined.")
} }
@ -313,7 +355,7 @@ func (c *config) getAliasMap() map[string][]string {
for i := range c.Tables { for i := range c.Tables {
t := c.Tables[i] t := c.Tables[i]
if len(t.Table) == 0 { if len(t.Table) == 0 || len(t.Columns) != 0 {
continue continue
} }
@ -348,3 +390,31 @@ func sanitize(s string) string {
return strings.ToLower(m) return strings.ToLower(m)
}) })
} }
func getConfigName() string {
if len(os.Getenv("GO_ENV")) == 0 {
return "dev"
}
ge := strings.ToLower(os.Getenv("GO_ENV"))
switch {
case strings.HasPrefix(ge, "pro"):
return "prod"
case strings.HasPrefix(ge, "sta"):
return "stage"
case strings.HasPrefix(ge, "tes"):
return "test"
case strings.HasPrefix(ge, "dev"):
return "dev"
}
return ge
}
func isDev() bool {
return strings.HasPrefix(os.Getenv("GO_ENV"), "dev")
}

View File

@ -8,9 +8,61 @@ import (
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
) )
func addTables(c *config, di *psql.DBInfo) error {
for _, t := range c.Tables {
if len(t.Table) == 0 || len(t.Columns) == 0 {
continue
}
if err := addTable(di, t.Columns, t); err != nil {
return err
}
}
return nil
}
func addTable(di *psql.DBInfo, cols []configColumn, t configTable) error {
bc, ok := di.GetColumn(t.Table, t.Name)
if !ok {
return fmt.Errorf(
"Column '%s' not found on table '%s'",
t.Name, t.Table)
}
if bc.Type != "json" && bc.Type != "jsonb" {
return fmt.Errorf(
"Column '%s' in table '%s' is of type '%s'. Only JSON or JSONB is valid",
t.Name, t.Table, bc.Type)
}
table := psql.DBTable{
Name: t.Name,
Key: strings.ToLower(t.Name),
Type: bc.Type,
}
columns := make([]psql.DBColumn, 0, len(cols))
for i := range cols {
c := cols[i]
columns = append(columns, psql.DBColumn{
Name: c.Name,
Key: strings.ToLower(c.Name),
Type: c.Type,
})
}
di.AddTable(table, columns)
bc.FKeyTable = t.Name
return nil
}
func addForeignKeys(c *config, di *psql.DBInfo) error { func addForeignKeys(c *config, di *psql.DBInfo) error {
for _, t := range c.Tables { for _, t := range c.Tables {
for _, c := range t.Columns { for _, c := range t.Columns {
if len(c.ForeignKey) == 0 {
continue
}
if err := addForeignKey(di, c, t); err != nil { if err := addForeignKey(di, c, t); err != nil {
return err return err
} }
@ -23,7 +75,7 @@ func addForeignKey(di *psql.DBInfo, c configColumn, t configTable) error {
c1, ok := di.GetColumn(t.Name, c.Name) c1, ok := di.GetColumn(t.Name, c.Name)
if !ok { if !ok {
return fmt.Errorf( return fmt.Errorf(
"Invalid table '%s' or column '%s in config", "Invalid table '%s' or column '%s' in config",
t.Name, c.Name) t.Name, c.Name)
} }

View File

@ -11,7 +11,9 @@ import (
"time" "time"
"github.com/cespare/xxhash/v2" "github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/allow"
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
"github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
) )
@ -107,7 +109,7 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *stmt, error) {
} }
ps, ok := _preparedList[gqlHash(c.req.Query, c.req.Vars, role)] ps, ok := _preparedList[stmtHash(allow.QueryName(c.req.Query), role)]
if !ok { if !ok {
return nil, nil, errUnauthorized return nil, nil, errUnauthorized
} }
@ -191,6 +193,8 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
} }
st := &stmts[0] st := &stmts[0]
//fmt.Println(">", string(st.sql))
t := fasttemplate.New(st.sql, openVar, closeVar) t := fasttemplate.New(st.sql, openVar, closeVar)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
@ -240,8 +244,14 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
} }
} }
if !conf.Production { if root, err = encryptCursor(st.qc, root); err != nil {
_allowList.add(&c.req) return nil, nil, err
}
if allowList.IsPersist() {
if err := allowList.Set(c.req.Vars, c.req.Query, c.req.ref); err != nil {
return nil, nil, err
}
} }
if len(stmts) > 1 { if len(stmts) > 1 {

72
serv/cursor.go Normal file
View File

@ -0,0 +1,72 @@
package serv
import (
"bytes"
"encoding/base64"
"github.com/dosco/super-graph/crypto"
"github.com/dosco/super-graph/jsn"
"github.com/dosco/super-graph/qcode"
)
func encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) {
var keys [][]byte
for _, s := range qc.Selects {
if s.Paging.Type != qcode.PtOffset {
var buf bytes.Buffer
buf.WriteString(s.FieldName)
buf.WriteString("_cursor")
keys = append(keys, buf.Bytes())
}
}
if len(keys) == 0 {
return data, nil
}
from := jsn.Get(data, keys)
to := make([]jsn.Field, len(from))
for i, f := range from {
to[i].Key = f.Key
if f.Value[0] != '"' || f.Value[len(f.Value)-1] != '"' {
continue
}
var buf bytes.Buffer
if len(f.Value) > 2 {
v, err := crypto.Encrypt(f.Value[1:len(f.Value)-1], &internalKey)
if err != nil {
return nil, err
}
buf.WriteByte('"')
buf.WriteString(base64.StdEncoding.EncodeToString(v))
buf.WriteByte('"')
} else {
buf.WriteString(`null`)
}
to[i].Value = buf.Bytes()
}
var buf bytes.Buffer
if err := jsn.Replace(&buf, data, from, to); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func decrypt(data string) ([]byte, error) {
v, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil, err
}
return crypto.Decrypt(v, &internalKey)
}

View File

@ -4,7 +4,7 @@ package serv
func Fuzz(data []byte) int { func Fuzz(data []byte) int {
gql := string(data) gql := string(data)
gqlName(gql) QueryName(gql)
gqlHash(gql, nil, "") gqlHash(gql, nil, "")
return 1 return 1

View File

@ -10,7 +10,6 @@ func TestFuzzCrashers(t *testing.T) {
} }
for _, f := range crashers { for _, f := range crashers {
_ = gqlName(f)
gqlHash(f, nil, "") gqlHash(f, nil, "")
} }
} }

178
serv/init.go Normal file
View File

@ -0,0 +1,178 @@
package serv
import (
"context"
"crypto/sha256"
"fmt"
"os"
"github.com/dosco/super-graph/allow"
"github.com/dosco/super-graph/crypto"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/rs/zerolog"
)
func initLog() {
out := zerolog.ConsoleWriter{Out: os.Stderr}
logger = zerolog.New(out).With().Timestamp().Logger()
errlog = logger.With().Caller().Logger()
}
func initConf() (*config, error) {
vi := newConfig(getConfigName())
if err := vi.ReadInConfig(); err != nil {
return nil, err
}
inherits := vi.GetString("inherits")
if len(inherits) != 0 {
vi = newConfig(inherits)
if err := vi.ReadInConfig(); err != nil {
return nil, err
}
if vi.IsSet("inherits") {
errlog.Fatal().Msgf("inherited config (%s) cannot itself inherit (%s)",
inherits,
vi.GetString("inherits"))
}
vi.SetConfigName(getConfigName())
if err := vi.MergeInConfig(); err != nil {
return nil, err
}
}
c := &config{}
if err := c.Init(vi); err != nil {
return nil, fmt.Errorf("unable to decode config, %v", err)
}
logLevel, err := zerolog.ParseLevel(c.LogLevel)
if err != nil {
errlog.Error().Err(err).Msg("error setting log_level")
}
zerolog.SetGlobalLevel(logLevel)
return c, nil
}
func initDB(c *config, useDB bool) (*pgx.Conn, error) {
config, _ := pgx.ParseConfig("")
config.Host = c.DB.Host
config.Port = c.DB.Port
config.User = c.DB.User
config.Password = c.DB.Password
config.RuntimeParams = map[string]string{
"application_name": c.AppName,
"search_path": c.DB.Schema,
}
if useDB {
config.Database = c.DB.DBName
}
switch c.LogLevel {
case "debug":
config.LogLevel = pgx.LogLevelDebug
case "info":
config.LogLevel = pgx.LogLevelInfo
case "warn":
config.LogLevel = pgx.LogLevelWarn
case "error":
config.LogLevel = pgx.LogLevelError
default:
config.LogLevel = pgx.LogLevelNone
}
config.Logger = NewSQLLogger(logger)
db, err := pgx.ConnectConfig(context.Background(), config)
if err != nil {
return nil, err
}
return db, nil
}
func initDBPool(c *config) (*pgxpool.Pool, error) {
config, _ := pgxpool.ParseConfig("")
config.ConnConfig.Host = c.DB.Host
config.ConnConfig.Port = c.DB.Port
config.ConnConfig.Database = c.DB.DBName
config.ConnConfig.User = c.DB.User
config.ConnConfig.Password = c.DB.Password
config.ConnConfig.RuntimeParams = map[string]string{
"application_name": c.AppName,
"search_path": c.DB.Schema,
}
switch c.LogLevel {
case "debug":
config.ConnConfig.LogLevel = pgx.LogLevelDebug
case "info":
config.ConnConfig.LogLevel = pgx.LogLevelInfo
case "warn":
config.ConnConfig.LogLevel = pgx.LogLevelWarn
case "error":
config.ConnConfig.LogLevel = pgx.LogLevelError
default:
config.ConnConfig.LogLevel = pgx.LogLevelNone
}
config.ConnConfig.Logger = NewSQLLogger(logger)
// if c.DB.MaxRetries != 0 {
// opt.MaxRetries = c.DB.MaxRetries
// }
if c.DB.PoolSize != 0 {
config.MaxConns = conf.DB.PoolSize
}
db, err := pgxpool.ConnectConfig(context.Background(), config)
if err != nil {
return nil, err
}
return db, nil
}
func initCompiler() {
var err error
qcompile, pcompile, err = initCompilers(conf)
if err != nil {
errlog.Fatal().Err(err).Msg("failed to initialize compilers")
}
}
func initAllowList(cpath string) {
var ac allow.Config
var err error
if !conf.Production {
ac = allow.Config{CreateIfNotExists: true, Persist: true}
}
allowList, err = allow.New(cpath, ac)
if err != nil {
errlog.Fatal().Err(err).Msg("failed to initialize allow list")
}
}
func initCrypto() {
if len(conf.SecretKey) != 0 {
secretKey = sha256.Sum256([]byte(conf.SecretKey))
conf.SecretKey = ""
internalKey = secretKey
} else {
internalKey = crypto.NewEncryptionKey()
}
}

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/dosco/super-graph/allow"
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
"github.com/jackc/pgconn" "github.com/jackc/pgconn"
"github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4"
@ -23,7 +24,10 @@ var (
_preparedList map[string]*preparedItem _preparedList map[string]*preparedItem
) )
func initPreparedList() { func initPreparedList(cpath string) {
if allowList.IsPersist() {
return
}
_preparedList = make(map[string]*preparedItem) _preparedList = make(map[string]*preparedItem)
tx, err := db.Begin(context.Background()) tx, err := db.Begin(context.Background())
@ -43,30 +47,38 @@ func initPreparedList() {
success := 0 success := 0
for _, v := range _allowList.list { list, err := allowList.Load()
if len(v.gql) == 0 { if err != nil {
errlog.Fatal().Err(err).Send()
}
for _, v := range list {
if len(v.Query) == 0 {
continue continue
} }
err := prepareStmt(v.gql, v.vars) err := prepareStmt(v)
if err == nil { if err == nil {
success++ success++
continue continue
} }
if len(v.vars) == 0 { if len(v.Vars) == 0 {
logger.Warn().Err(err).Msg(v.gql) logger.Warn().Err(err).Msg(v.Query)
} else { } else {
logger.Warn().Err(err).Msgf("%s %s", v.vars, v.gql) logger.Warn().Err(err).Msgf("%s %s", v.Vars, v.Query)
} }
} }
logger.Info(). logger.Info().
Msgf("Registered %d of %d queries from allow.list as prepared statements", Msgf("Registered %d of %d queries from allow.list as prepared statements",
success, len(_allowList.list)) success, len(list))
} }
func prepareStmt(gql string, vars []byte) error { func prepareStmt(item allow.Item) error {
gql := item.Query
vars := item.Vars
qt := qcode.GetQType(gql) qt := qcode.GetQType(gql)
q := []byte(gql) q := []byte(gql)
@ -99,7 +111,7 @@ func prepareStmt(gql string, vars []byte) error {
logger.Debug().Msg("Prepared statement role: user") logger.Debug().Msg("Prepared statement role: user")
err = prepare(tx, stmts1, gqlHash(gql, vars, "user")) err = prepare(tx, stmts1, stmtHash(item.Name, "user"))
if err != nil { if err != nil {
return err return err
} }
@ -112,7 +124,7 @@ func prepareStmt(gql string, vars []byte) error {
return err return err
} }
err = prepare(tx, stmts2, gqlHash(gql, vars, "anon")) err = prepare(tx, stmts2, stmtHash(item.Name, "anon"))
if err != nil { if err != nil {
return err return err
} }
@ -127,7 +139,7 @@ func prepareStmt(gql string, vars []byte) error {
return err return err
} }
err = prepare(tx, stmts, gqlHash(gql, vars, role.Name)) err = prepare(tx, stmts, stmtHash(item.Name, role.Name))
if err != nil { if err != nil {
return err return err
} }

View File

@ -108,7 +108,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
// Ensure that we use the correct events, as they are not uniform across // Ensure that we use the correct events, as they are not uniform across
// platforms. See https://github.com/fsnotify/fsnotify/issues/74 // platforms. See https://github.com/fsnotify/fsnotify/issues/74
if !conf.Production && strings.HasSuffix(event.Name, "/allow.list") { if conf != nil && !conf.Production && strings.HasSuffix(event.Name, "/allow.list") {
continue continue
} }

View File

@ -22,16 +22,20 @@ type resolvFn struct {
Fn func(h http.Header, id []byte) ([]byte, error) Fn func(h http.Header, id []byte) ([]byte, error)
} }
func initResolvers() error { func initResolvers() {
var err error
rmap = make(map[uint64]*resolvFn) rmap = make(map[uint64]*resolvFn)
for _, t := range conf.Tables { for _, t := range conf.Tables {
err := initRemotes(t) err = initRemotes(t)
if err != nil { if err != nil {
return err break
} }
} }
return nil
if err != nil {
errlog.Fatal().Err(err).Msg("failed to initialize resolvers")
}
} }
func initRemotes(t configTable) error { func initRemotes(t configTable) error {

View File

@ -21,6 +21,10 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
return nil, nil, err return nil, nil, err
} }
if err = addTables(c, di); err != nil {
return nil, nil, err
}
if err = addForeignKeys(c, di); err != nil { if err = addForeignKeys(c, di); err != nil {
return nil, nil, err return nil, nil, err
} }
@ -50,7 +54,7 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
} }
func initWatcher(cpath string) { func initWatcher(cpath string) {
if !conf.WatchAndReload { if conf != nil && !conf.WatchAndReload {
return return
} }
@ -70,8 +74,17 @@ func initWatcher(cpath string) {
} }
func startHTTP() { func startHTTP() {
var hostPort string
var appName string
defaultHP := "0.0.0.0:8080"
env := os.Getenv("GO_ENV")
if conf != nil {
appName = conf.AppName
hp := strings.SplitN(conf.HostPort, ":", 2) hp := strings.SplitN(conf.HostPort, ":", 2)
if len(hp) == 2 {
if len(conf.Host) != 0 { if len(conf.Host) != 0 {
hp[0] = conf.Host hp[0] = conf.Host
} }
@ -80,11 +93,22 @@ func startHTTP() {
hp[1] = conf.Port hp[1] = conf.Port
} }
hostPort := fmt.Sprintf("%s:%s", hp[0], hp[1]) hostPort = fmt.Sprintf("%s:%s", hp[0], hp[1])
}
}
if len(hostPort) == 0 {
hostPort = defaultHP
}
routes, err := routeHandler()
if err != nil {
errlog.Fatal().Err(err).Send()
}
srv := &http.Server{ srv := &http.Server{
Addr: hostPort, Addr: hostPort,
Handler: routeHandler(), Handler: routes,
ReadTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
@ -110,8 +134,8 @@ func startHTTP() {
Str("version", version). Str("version", version).
Str("git_branch", gitBranch). Str("git_branch", gitBranch).
Str("host_post", hostPort). Str("host_post", hostPort).
Str("app_name", conf.AppName). Str("app_name", appName).
Str("env", conf.Env). Str("env", env).
Msgf("%s listening", serverName) Msgf("%s listening", serverName)
if err := srv.ListenAndServe(); err != http.ErrServerClosed { if err := srv.ListenAndServe(); err != http.ErrServerClosed {
@ -121,22 +145,35 @@ func startHTTP() {
<-idleConnsClosed <-idleConnsClosed
} }
func routeHandler() http.Handler { func routeHandler() (http.Handler, error) {
var apiH http.Handler mux := http.NewServeMux()
if conf.HTTPGZip { if conf == nil {
gzipH := gziphandler.MustNewGzipLevelHandler(6) return mux, nil
apiH = gzipH(http.HandlerFunc(apiV1))
} else {
apiH = http.HandlerFunc(apiV1)
} }
mux := http.NewServeMux() routes := map[string]http.Handler{
mux.HandleFunc("/health", health) "/health": http.HandlerFunc(health),
mux.Handle("/api/v1/graphql", withAuth(apiH)) "/api/v1/graphql": withAuth(http.HandlerFunc(apiV1), conf.Auth),
}
if err := setActionRoutes(routes); err != nil {
return nil, err
}
if conf.WebUI { if conf.WebUI {
mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox())) routes["/"] = http.FileServer(rice.MustFindBox("../web/build").HTTPBox())
}
if conf.HTTPGZip {
gz := gziphandler.MustNewGzipLevelHandler(6)
for k, v := range routes {
routes[k] = gz(v)
}
}
for k, v := range routes {
mux.Handle(k, v)
} }
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
@ -144,29 +181,38 @@ func routeHandler() http.Handler {
mux.ServeHTTP(w, r) mux.ServeHTTP(w, r)
} }
return http.HandlerFunc(fn) return http.HandlerFunc(fn), nil
} }
func getConfigName() string { func setActionRoutes(routes map[string]http.Handler) error {
if len(os.Getenv("GO_ENV")) == 0 { var err error
return "dev"
for _, a := range conf.Actions {
var fn http.Handler
fn, err = newAction(a)
if err != nil {
break
} }
ge := strings.ToLower(os.Getenv("GO_ENV")) p := fmt.Sprintf("/api/v1/actions/%s", strings.ToLower(a.Name))
switch { if authc, ok := findAuth(a.AuthName); ok {
case strings.HasPrefix(ge, "pro"): routes[p] = withAuth(fn, authc)
return "prod" } else {
routes[p] = fn
case strings.HasPrefix(ge, "sta"): }
return "stage" }
return nil
case strings.HasPrefix(ge, "tes"):
return "test"
case strings.HasPrefix(ge, "dev"):
return "dev"
} }
return ge func findAuth(name string) (configAuth, bool) {
var authc configAuth
for _, a := range conf.Auths {
if strings.EqualFold(a.Name, name) {
return a, true
}
}
return authc, false
} }

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"sort" "sort"
"strings" "strings"
"sync"
"github.com/cespare/xxhash/v2" "github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/jsn" "github.com/dosco/super-graph/jsn"
@ -22,6 +23,14 @@ func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 {
return v return v
} }
// nolint: errcheck
func stmtHash(name string, role string) string {
h := sha1.New()
io.WriteString(h, strings.ToLower(name))
io.WriteString(h, role)
return hex.EncodeToString(h.Sum(nil))
}
// nolint: errcheck // nolint: errcheck
func gqlHash(b string, vars []byte, role string) string { func gqlHash(b string, vars []byte, role string) string {
b = strings.TrimSpace(b) b = strings.TrimSpace(b)
@ -108,30 +117,6 @@ func al(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
} }
func gqlName(b string) string {
state, s := 0, 0
for i := 0; i < len(b); i++ {
switch {
case state == 2 && b[i] == '{':
return b[s:i]
case state == 2 && b[i] == ' ':
return b[s:i]
case state == 1 && b[i] == '{':
return ""
case state == 1 && b[i] != ' ':
s = i
state = 2
case state == 1 && b[i] == ' ':
continue
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
state = 1
}
}
return ""
}
func findStmt(role string, stmts []stmt) *stmt { func findStmt(role string, stmts []stmt) *stmt {
for i := range stmts { for i := range stmts {
if stmts[i].role.Name != role { if stmts[i].role.Name != role {
@ -141,3 +126,16 @@ func findStmt(role string, stmts []stmt) *stmt {
} }
return nil return nil
} }
func fatalInProd(err error, msg string) {
var wg sync.WaitGroup
if !isDev() {
errlog.Fatal().Err(err).Msg(msg)
}
errlog.Error().Err(err).Msg(msg)
wg.Add(1)
wg.Wait()
}

View File

@ -229,80 +229,3 @@ func TestGQLHashWithVars2(t *testing.T) {
t.Fatal("Hashes don't match they should") t.Fatal("Hashes don't match they should")
} }
} }
func TestGQLName1(t *testing.T) {
var q = `
query {
products(
distinct: [price]
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
) { id name } }`
name := gqlName(q)
if len(name) != 0 {
t.Fatal("Name should be empty, not ", name)
}
}
func TestGQLName2(t *testing.T) {
var q = `
query hakuna_matata {
products(
distinct: [price]
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
) {
id
name
}
}`
name := gqlName(q)
if name != "hakuna_matata" {
t.Fatal("Name should be 'hakuna_matata', not ", name)
}
}
func TestGQLName3(t *testing.T) {
var q = `
mutation means{ users { id } }`
// var v2 = ` { products( limit: 30, order_by: { price: desc }, distinct: [ price ] where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) { id name price user { id email } } } `
name := gqlName(q)
if name != "means" {
t.Fatal("Name should be 'means', not ", name)
}
}
func TestGQLName4(t *testing.T) {
var q = `
query no_worries
users {
id
}
}`
name := gqlName(q)
if name != "no_worries" {
t.Fatal("Name should be 'no_worries', not ", name)
}
}
func TestGQLName5(t *testing.T) {
var q = `
{
users {
id
}
}`
name := gqlName(q)
if len(name) != 0 {
t.Fatal("Name should be empty, not ", name)
}
}

View File

@ -32,6 +32,10 @@ reload_on_config_change: true
# Path pointing to where the migrations can be found # Path pointing to where the migrations can be found
migrations_path: ./config/migrations migrations_path: ./config/migrations
# Secret key for general encryption operations like
# encrypting the cursor data
secret_key: supercalifajalistics
# Postgres related environment Variables # Postgres related environment Variables
# SG_DATABASE_HOST # SG_DATABASE_HOST
# SG_DATABASE_PORT # SG_DATABASE_PORT
@ -49,7 +53,7 @@ migrations_path: ./config/migrations
# sheep: sheep # sheep: sheep
auth: auth:
# Can be 'rails' or 'jwt' # Can be 'rails', 'jwt' or 'header'
type: rails type: rails
cookie: _{% app_name_slug %}_session cookie: _{% app_name_slug %}_session
@ -83,6 +87,22 @@ auth:
# public_key_file: /secrets/public_key.pem # public_key_file: /secrets/public_key.pem
# public_key_type: ecdsa #rsa # public_key_type: ecdsa #rsa
# header:
# name: dnt
# exists: true
# value: localhost:8080
# You can add additional named auths to use with actions
# In this example actions using this auth can only be
# called from the Google Appengine Cron service that
# sets a special header to all it's requests
auths:
- name: from_taskqueue
type: header
header:
name: X-Appengine-Cron
exists: true
database: database:
type: postgres type: postgres
host: db host: db
@ -116,6 +136,16 @@ database:
- encrypted - encrypted
- token - token
# Create custom actions with their own api endpoints
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
# A request to this url will execute the configured SQL query
# which in this case refreshes a materialized view in the database.
# The auth_name is from one of the configured auths
actions:
- name: refresh_leaderboard_users
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
auth_name: from_taskqueue
tables: tables:
- name: customers - name: customers
remotes: remotes:
@ -137,6 +167,7 @@ tables:
name: me name: me
table: users table: users
roles_query: "SELECT * FROM users WHERE id = $user_id" roles_query: "SELECT * FROM users WHERE id = $user_id"
roles: roles:
@ -168,20 +199,16 @@ roles:
query: query:
limit: 50 limit: 50
filters: ["{ user_id: { eq: $user_id } }"] filters: ["{ user_id: { eq: $user_id } }"]
columns: ["id", "name", "description" ]
disable_functions: false disable_functions: false
insert: insert:
filters: ["{ user_id: { eq: $user_id } }"] filters: ["{ user_id: { eq: $user_id } }"]
columns: ["id", "name", "description" ]
presets: presets:
- user_id: "$user_id"
- created_at: "now" - created_at: "now"
update: update:
filters: ["{ user_id: { eq: $user_id } }"] filters: ["{ user_id: { eq: $user_id } }"]
columns:
- id
- name
presets: presets:
- updated_at: "now" - updated_at: "now"

View File

@ -1,14 +1,53 @@
version: '3.4' version: '3.4'
services: services:
# Postgres DB
db: db:
image: postgres image: postgres:11.5
ports: ports:
- "5432:5432" - "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: {% app_name_slug %}_api:
image: dosco/super-graph:latest image: dosco/super-graph:latest
environment: environment:
GO_ENV: "development" GO_ENV: "development"
# Uncomment below for Yugabyte DB
# SG_DATABASE_PORT: 5433
# SG_DATABASE_USER: yugabyte
# SG_DATABASE_PASSWORD: yugabyte
volumes: volumes:
- ./config:/config - ./config:/config
ports: ports:

View File

@ -32,6 +32,10 @@ enable_tracing: true
# Path pointing to where the migrations can be found # Path pointing to where the migrations can be found
# migrations_path: migrations # migrations_path: migrations
# Secret key for general encryption operations like
# encrypting the cursor data
# secret_key: supercalifajalistics
# Postgres related environment Variables # Postgres related environment Variables
# SG_DATABASE_HOST # SG_DATABASE_HOST
# SG_DATABASE_PORT # SG_DATABASE_PORT