Add pagination using opaque cursors
This commit is contained in:
parent
12007db76e
commit
7413813138
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1024,6 +1024,75 @@ mutation {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products(first: 10, after: "MJoTLbQF4l0GuoDsYmCrpjPeaaIlNpfm4uFU4PQ=") {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Paginating backward through your results
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products(last: 10, before: "MJoTLbQF4l0GuoDsYmCrpjPeaaIlNpfm4uFU4PQ=") {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Using Variables
|
## 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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -158,6 +158,9 @@ var (
|
||||||
|
|
||||||
input5 = `
|
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",}`
|
{"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) {
|
||||||
|
@ -227,6 +230,32 @@ func TestGet1(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -12,20 +12,11 @@ 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) {
|
||||||
|
@ -36,20 +27,11 @@ func singleInsert(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -60,20 +42,11 @@ func bulkInsert(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
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"= ((i.j->'product'->'connect'->>'id'))::bigint) 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) {
|
||||||
|
@ -290,8 +214,6 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `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_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", "tags_2_join"."json_2" AS "tags") AS "json_row_0")) AS "json_0" 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("json_2"), '[]') AS "json_2" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "tags_2"."id" AS "id", "tags_2"."name" AS "name") AS "json_row_2")) AS "json_2" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2" LIMIT ('20') :: integer) AS "json_agg_2") AS "tags_2_join" ON ('true') 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",
|
||||||
|
@ -304,14 +226,7 @@ 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 {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedInsertOneToOneWithConnectArray(t *testing.T) {
|
func nestedInsertOneToOneWithConnectArray(t *testing.T) {
|
||||||
|
@ -327,8 +242,6 @@ func nestedInsertOneToOneWithConnectArray(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `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_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",
|
||||||
|
@ -341,14 +254,7 @@ func nestedInsertOneToOneWithConnectArray(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 TestCompileInsert(t *testing.T) {
|
func TestCompileInsert(t *testing.T) {
|
||||||
|
@ -362,5 +268,4 @@ func TestCompileInsert(t *testing.T) {
|
||||||
t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect)
|
t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect)
|
||||||
t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect)
|
t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect)
|
||||||
t.Run("nestedInsertOneToOneWithConnectArray", nestedInsertOneToOneWithConnectArray)
|
t.Run("nestedInsertOneToOneWithConnectArray", nestedInsertOneToOneWithConnectArray)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -682,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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package psql
|
package psql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dosco/super-graph/qcode"
|
"github.com/dosco/super-graph/qcode"
|
||||||
|
@ -10,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) {
|
||||||
|
@ -138,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
578
psql/query.go
578
psql/query.go
|
@ -21,16 +21,22 @@ type Variables map[string]json.RawMessage
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Schema *DBSchema
|
Schema *DBSchema
|
||||||
|
Decryptor func(string) ([]byte, error)
|
||||||
Vars map[string]string
|
Vars map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Compiler struct {
|
type Compiler struct {
|
||||||
schema *DBSchema
|
schema *DBSchema
|
||||||
|
decryptor func(string) ([]byte, error)
|
||||||
vars map[string]string
|
vars map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompiler(conf Config) *Compiler {
|
func NewCompiler(conf Config) *Compiler {
|
||||||
return &Compiler{conf.Schema, conf.Vars}
|
return &Compiler{
|
||||||
|
schema: conf.Schema,
|
||||||
|
decryptor: conf.Decryptor,
|
||||||
|
vars: conf.Vars,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error {
|
func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error {
|
||||||
|
@ -79,14 +85,11 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &compilerContext{w, qc.Selects, co}
|
c := &compilerContext{w, qc.Selects, co}
|
||||||
multiRoot := (len(qc.Roots) > 1)
|
|
||||||
|
|
||||||
st := NewIntStack()
|
st := NewIntStack()
|
||||||
si := 0
|
i := 0
|
||||||
|
|
||||||
if multiRoot {
|
|
||||||
io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `)
|
|
||||||
|
|
||||||
|
io.WriteString(c.w, `SELECT json_build_object(`)
|
||||||
for _, id := range qc.Roots {
|
for _, id := range qc.Roots {
|
||||||
root := qc.Selects[id]
|
root := qc.Selects[id]
|
||||||
if root.SkipRender {
|
if root.SkipRender {
|
||||||
|
@ -96,44 +99,31 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||||
st.Push(root.ID + closeBlock)
|
st.Push(root.ID + closeBlock)
|
||||||
st.Push(root.ID)
|
st.Push(root.ID)
|
||||||
|
|
||||||
if si != 0 {
|
if i != 0 {
|
||||||
io.WriteString(c.w, `, `)
|
io.WriteString(c.w, `, `)
|
||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(c.w, `"sel_`)
|
|
||||||
int2string(c.w, root.ID)
|
|
||||||
io.WriteString(c.w, `"."json_`)
|
|
||||||
int2string(c.w, root.ID)
|
|
||||||
io.WriteString(c.w, `"`)
|
|
||||||
|
|
||||||
alias(c.w, root.FieldName)
|
|
||||||
si++
|
|
||||||
}
|
|
||||||
|
|
||||||
if si != 0 {
|
|
||||||
io.WriteString(c.w, ` FROM `)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
root := qc.Selects[0]
|
|
||||||
if !root.SkipRender {
|
|
||||||
io.WriteString(c.w, `SELECT json_object_agg(`)
|
|
||||||
io.WriteString(c.w, `'`)
|
io.WriteString(c.w, `'`)
|
||||||
io.WriteString(c.w, root.FieldName)
|
io.WriteString(c.w, root.FieldName)
|
||||||
io.WriteString(c.w, `', `)
|
io.WriteString(c.w, `', `)
|
||||||
io.WriteString(c.w, `json_`)
|
io.WriteString(c.w, `"sel_`)
|
||||||
int2string(c.w, root.ID)
|
int2string(c.w, root.ID)
|
||||||
|
io.WriteString(c.w, `"."json"`)
|
||||||
|
|
||||||
st.Push(root.ID + closeBlock)
|
if root.Paging.Type != qcode.PtOffset {
|
||||||
st.Push(root.ID)
|
io.WriteString(c.w, `, '`)
|
||||||
|
io.WriteString(c.w, root.FieldName)
|
||||||
io.WriteString(c.w, `) FROM `)
|
io.WriteString(c.w, `_cursor', "sel_`)
|
||||||
si++
|
int2string(c.w, root.ID)
|
||||||
}
|
io.WriteString(c.w, `"."__cursor"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if si == 0 {
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(c.w, `) as "__root" FROM `)
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
return 0, errors.New("all tables skipped. cannot render query")
|
return 0, errors.New("all tables skipped. cannot render query")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,19 +139,15 @@ 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)
|
skipped, err := c.renderSelect(sel, ti)
|
||||||
|
@ -186,45 +172,31 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||||
} else {
|
} else {
|
||||||
sel := &c.s[(id - closeBlock)]
|
sel := &c.s[(id - closeBlock)]
|
||||||
|
|
||||||
ti, err := c.schema.GetTable(sel.Name)
|
if sel.ParentID == -1 {
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.renderSelectClose(sel, ti)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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, error) {
|
func (c *compilerContext) initSelector(sel *qcode.Select, ti *DBTableInfo) (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))
|
||||||
|
@ -238,6 +210,40 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
|
||||||
colmap[sel.OrderBy[i].Col] = struct{}{}
|
colmap[sel.OrderBy[i].Col] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sel.Paging.Type != qcode.PtOffset {
|
||||||
|
colmap[ti.PrimaryCol.Key] = struct{}{}
|
||||||
|
|
||||||
|
var filOrder qcode.Order
|
||||||
|
var filOp qcode.ExpOp
|
||||||
|
|
||||||
|
switch sel.Paging.Type {
|
||||||
|
case qcode.PtForward:
|
||||||
|
filOrder = qcode.OrderAsc
|
||||||
|
filOp = qcode.OpGreaterThan
|
||||||
|
|
||||||
|
case qcode.PtBackward:
|
||||||
|
filOrder = qcode.OrderDesc
|
||||||
|
filOp = qcode.OpLesserThan
|
||||||
|
}
|
||||||
|
|
||||||
|
sel.OrderBy = append(sel.OrderBy, &qcode.OrderBy{
|
||||||
|
Col: ti.PrimaryCol.Name,
|
||||||
|
Order: filOrder,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(sel.Paging.Cursor) != 0 {
|
||||||
|
v, err := c.decryptor(sel.Paging.Cursor)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fil := qcode.AddFilter(sel)
|
||||||
|
fil.Op = filOp
|
||||||
|
fil.Col = ti.PrimaryCol.Name
|
||||||
|
fil.Val = string(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, id := range sel.Children {
|
for _, id := range sel.Children {
|
||||||
child := &c.s[id]
|
child := &c.s[id]
|
||||||
|
|
||||||
|
@ -296,131 +302,58 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
skipped, childCols, err := c.processChildren(sel, ti)
|
skipped, childCols, err := c.initSelector(sel, ti)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
hasOrder := len(sel.OrderBy) != 0
|
|
||||||
|
|
||||||
// SELECT
|
// SELECT
|
||||||
if !ti.Singular {
|
if !ti.Singular {
|
||||||
//fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Name)
|
io.WriteString(c.w, `SELECT coalesce(json_agg(json_build_object(`)
|
||||||
io.WriteString(c.w, `SELECT coalesce(json_agg("`)
|
if err := c.renderColumns(sel, ti, skipped); err != nil {
|
||||||
io.WriteString(c.w, "json_")
|
|
||||||
int2string(c.w, sel.ID)
|
|
||||||
io.WriteString(c.w, `"`)
|
|
||||||
|
|
||||||
if hasOrder {
|
|
||||||
if err := c.renderOrderBy(sel, ti); err != nil {
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
io.WriteString(c.w, `)), '[]') AS "json"`)
|
||||||
|
|
||||||
|
if sel.Paging.Type != qcode.PtOffset {
|
||||||
|
io.WriteString(c.w, `, max(`)
|
||||||
|
colWithTableID(c.w, ti.Name, sel.ID, ti.PrimaryCol.Name)
|
||||||
|
io.WriteString(c.w, `) AS "__cursor"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
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"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Name)
|
|
||||||
io.WriteString(c.w, `), '[]')`)
|
|
||||||
aliasWithID(c.w, "json", sel.ID)
|
|
||||||
io.WriteString(c.w, ` FROM (`)
|
io.WriteString(c.w, ` FROM (`)
|
||||||
}
|
|
||||||
|
|
||||||
// ROW-TO-JSON
|
|
||||||
io.WriteString(c.w, `SELECT `)
|
|
||||||
|
|
||||||
if len(sel.DistinctOn) != 0 {
|
|
||||||
c.renderDistinctOn(sel, ti)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if err = c.renderJoinedColumns(sel, ti, skipped); 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 .... )
|
// FROM (SELECT .... )
|
||||||
err = c.renderBaseSelect(sel, ti, rel, childCols, skipped)
|
err = c.renderBaseSelect(sel, ti, rel, childCols, skipped)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return skipped, err
|
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
|
// END-FROM
|
||||||
|
|
||||||
return skipped, nil
|
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 {
|
|
||||||
case ti.Singular:
|
|
||||||
io.WriteString(c.w, ` LIMIT ('1') :: integer`)
|
|
||||||
|
|
||||||
case len(sel.Paging.Limit) != 0:
|
|
||||||
//fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit)
|
|
||||||
io.WriteString(c.w, ` LIMIT ('`)
|
|
||||||
io.WriteString(c.w, sel.Paging.Limit)
|
|
||||||
io.WriteString(c.w, `') :: integer`)
|
|
||||||
|
|
||||||
case sel.Paging.NoLimit:
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
io.WriteString(c.w, ` LIMIT ('20') :: integer`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(sel.Paging.Offset) != 0 {
|
|
||||||
//fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, c.sel.Paging.Offset)
|
|
||||||
io.WriteString(c.w, `OFFSET ('`)
|
|
||||||
io.WriteString(c.w, sel.Paging.Offset)
|
|
||||||
io.WriteString(c.w, `') :: integer`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ti.Singular {
|
|
||||||
//fmt.Fprintf(w, `) AS "json_agg_%d"`, c.sel.ID)
|
|
||||||
io.WriteString(c.w, `)`)
|
|
||||||
aliasWithID(c.w, "json_agg", sel.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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
|
||||||
}
|
}
|
||||||
|
@ -460,7 +393,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)
|
||||||
|
@ -484,15 +417,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.renderJoinedColumns(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]
|
||||||
|
@ -504,18 +443,19 @@ 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) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32, colsRendered int) error {
|
||||||
|
|
||||||
// columns previously rendered
|
// columns previously rendered
|
||||||
i := len(sel.Cols)
|
i := colsRendered
|
||||||
|
|
||||||
for _, id := range sel.Children {
|
for _, id := range sel.Children {
|
||||||
if hasBit(skipped, uint32(id)) {
|
if hasBit(skipped, uint32(id)) {
|
||||||
|
@ -530,18 +470,19 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo
|
||||||
io.WriteString(c.w, ", ")
|
io.WriteString(c.w, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
|
squoted(c.w, childSel.FieldName)
|
||||||
//s.Name, s.ID, s.Name, s.FieldName)
|
io.WriteString(c.w, `, "sel_`)
|
||||||
//if cti.Singular {
|
|
||||||
io.WriteString(c.w, `"`)
|
|
||||||
io.WriteString(c.w, childSel.Name)
|
|
||||||
io.WriteString(c.w, `_`)
|
|
||||||
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++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -550,171 +491,25 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo
|
||||||
|
|
||||||
func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, rel *DBRel,
|
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 := (rel == nil)
|
||||||
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
|
|
||||||
|
|
||||||
colmap := make(map[string]struct{}, (len(sel.Cols) + len(sel.OrderBy)))
|
io.WriteString(c.w, `SELECT `)
|
||||||
|
|
||||||
io.WriteString(c.w, ` FROM (SELECT `)
|
if len(sel.DistinctOn) != 0 {
|
||||||
|
c.renderDistinctOn(sel, ti)
|
||||||
i := 0
|
|
||||||
for n, col := range sel.Cols {
|
|
||||||
cn := col.Name
|
|
||||||
colmap[cn] = struct{}{}
|
|
||||||
|
|
||||||
_, 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 _, ob := range sel.OrderBy {
|
realColsRendered, isAgg, err := c.renderBaseColumns(sel, ti, childCols, skipped)
|
||||||
if _, ok := colmap[ob.Col]; ok {
|
if err != nil {
|
||||||
continue
|
return err
|
||||||
}
|
|
||||||
colmap[ob.Col] = struct{}{}
|
|
||||||
|
|
||||||
if i != 0 {
|
|
||||||
io.WriteString(c.w, `, `)
|
|
||||||
}
|
|
||||||
colWithTable(c.w, ti.Name, ob.Col)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, col := range childCols {
|
|
||||||
if _, ok := colmap[col.Name]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if i != 0 {
|
|
||||||
io.WriteString(c.w, `, `)
|
|
||||||
}
|
|
||||||
|
|
||||||
//fmt.Fprintf(w, `"%s"."%s"`, col.Table, col.Name)
|
|
||||||
colWithTable(c.w, col.Table, col.Name)
|
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(c.w, ` FROM `)
|
io.WriteString(c.w, ` FROM `)
|
||||||
|
|
||||||
c.renderFrom(sel, ti, rel)
|
c.renderFrom(sel, ti, rel)
|
||||||
|
|
||||||
// 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 (`)
|
||||||
if err := c.renderWhere(sel, ti); err != nil {
|
if err := c.renderWhere(sel, ti); err != nil {
|
||||||
|
@ -741,18 +536,21 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
|
||||||
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 {
|
||||||
|
err := c.renderOrderBy(sel, ti)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -779,10 +577,6 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
|
||||||
io.WriteString(c.w, `') :: integer`)
|
io.WriteString(c.w, `') :: integer`)
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID)
|
|
||||||
io.WriteString(c.w, `)`)
|
|
||||||
aliasWithID(c.w, ti.Name, sel.ID)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -824,23 +618,6 @@ func (c *compilerContext) renderFrom(sel *qcode.Select, ti *DBTableInfo, rel *DB
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInfo) {
|
|
||||||
//colsRendered := len(sel.Cols) != 0
|
|
||||||
|
|
||||||
for i := range sel.OrderBy {
|
|
||||||
//io.WriteString(w, ", ")
|
|
||||||
io.WriteString(c.w, `, `)
|
|
||||||
|
|
||||||
col := sel.OrderBy[i].Col
|
|
||||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`,
|
|
||||||
//c.sel.Name, c.sel.ID, c,
|
|
||||||
//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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
||||||
parent := c.s[sel.ParentID]
|
parent := c.s[sel.ParentID]
|
||||||
|
|
||||||
|
@ -961,7 +738,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(')')
|
||||||
|
@ -972,12 +748,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 {
|
||||||
|
@ -992,14 +768,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
|
||||||
|
@ -1161,31 +936,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, sel.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, sel.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, sel.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, sel.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, sel.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, sel.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)
|
||||||
|
@ -1200,8 +964,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, `) `)
|
||||||
}
|
}
|
||||||
|
@ -1225,36 +988,23 @@ func (c *compilerContext) renderList(ex *qcode.Exp) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *DBColumn) {
|
func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *DBColumn) {
|
||||||
io.WriteString(c.w, ` `)
|
|
||||||
|
|
||||||
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, ` '`)
|
io.WriteString(c.w, ` '`)
|
||||||
|
|
||||||
case qcode.ValVar:
|
if ex.Type == 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)
|
io.WriteString(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, `}}`)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
io.WriteString(c.w, ex.Val)
|
||||||
|
}
|
||||||
|
|
||||||
io.WriteString(c.w, `' :: `)
|
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 {
|
||||||
|
@ -1303,15 +1053,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)
|
||||||
|
@ -1332,27 +1073,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"
|
||||||
|
|
|
@ -28,16 +28,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,68 +81,7 @@ 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) {
|
||||||
|
@ -128,16 +92,7 @@ func fetchByID(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"."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) {
|
||||||
|
@ -150,16 +105,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 +119,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,16 +133,7 @@ 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) {
|
||||||
|
@ -227,16 +155,7 @@ func oneToManyArray(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
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 +169,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 +183,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 +194,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 +205,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 +216,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 +227,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 +237,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 +248,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 +265,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,16 +289,7 @@ 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 {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonColumnAsTable(t *testing.T) {
|
func jsonColumnAsTable(t *testing.T) {
|
||||||
|
@ -477,16 +306,7 @@ func jsonColumnAsTable(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", "tag_count_1_join"."json_1" AS "tag_count") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "tag_count_1"."count" AS "count", "tags_2_join"."json_2" AS "tags") AS "json_row_1")) AS "json_1" 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("json_2"), '[]') AS "json_2" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "tags_2"."name" AS "name") AS "json_row_2")) AS "json_2" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2" LIMIT ('20') :: integer) AS "json_agg_2") AS "tags_2_join" ON ('true') LIMIT ('1') :: integer) AS "tag_count_1_join" ON ('true') 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 skipUserIDForAnonRole(t *testing.T) {
|
func skipUserIDForAnonRole(t *testing.T) {
|
||||||
|
@ -501,16 +321,7 @@ func skipUserIDForAnonRole(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") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" 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 blockedQuery(t *testing.T) {
|
func blockedQuery(t *testing.T) {
|
||||||
|
@ -522,16 +333,7 @@ func blockedQuery(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -542,16 +344,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) {
|
||||||
|
|
|
@ -244,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 ""
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
=== 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(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" 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") IS NOT DISTINCT FROM '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.00s)
|
||||||
|
--- 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(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"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."id") < '28' :: bigint) AND (("products"."id") >= '20' :: bigint)))) ORDER BY "products"."price" DESC LIMIT ('30') :: integer) AS "products_0") AS "sel_0"
|
||||||
|
=== RUN TestCompileQuery/withWhereAndList
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "sel_0"
|
||||||
|
=== RUN TestCompileQuery/withWhereIsNull
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "sel_0"
|
||||||
|
=== RUN TestCompileQuery/withWhereMultiOr
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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"
|
||||||
|
=== 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") = '15' :: 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(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('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") AS "sel_0"
|
||||||
|
=== RUN TestCompileQuery/oneToMany
|
||||||
|
SELECT json_build_object('users', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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(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" ON ('true')) AS "sel_0"
|
||||||
|
=== RUN TestCompileQuery/oneToManyReverse
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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(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" ON ('true')) 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(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" ON ('true')) AS "sel_2", (SELECT coalesce(json_agg(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"
|
||||||
|
=== RUN TestCompileQuery/manyToMany
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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(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" ON ('true')) AS "sel_0"
|
||||||
|
=== RUN TestCompileQuery/manyToManyReverse
|
||||||
|
SELECT json_build_object('customers', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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(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" ON ('true')) AS "sel_0"
|
||||||
|
=== RUN TestCompileQuery/aggFunction
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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"
|
||||||
|
=== RUN TestCompileQuery/aggFunctionBlockedByCol
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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"
|
||||||
|
=== RUN TestCompileQuery/aggFunctionDisabled
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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"
|
||||||
|
=== RUN TestCompileQuery/aggFunctionWithFilter
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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"
|
||||||
|
=== 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") IS NOT DISTINCT FROM '{{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") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") IS NOT DISTINCT FROM '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint)))) 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(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"
|
||||||
|
=== 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(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" 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(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(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" ON ('true')) AS "sel_1" ON ('true')) AS "sel_0"
|
||||||
|
=== RUN TestCompileQuery/skipUserIDForAnonRole
|
||||||
|
SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(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"
|
||||||
|
=== 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(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"
|
||||||
|
--- 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/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") IS NOT DISTINCT FROM '1' :: bigint) AND (("products"."id") = '15' :: 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") IS NOT DISTINCT FROM '{{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") = '5' :: 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"
|
||||||
|
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' :: 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"
|
||||||
|
=== 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") IS NOT DISTINCT FROM '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") = '6' :: 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") = '6' :: 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") = '9' :: 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") = '9' :: 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") = '2' :: 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.127s
|
|
@ -13,20 +13,11 @@ func singleUpdate(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
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,20 +27,11 @@ 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) {
|
||||||
|
@ -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"= ((i.j->'product'->'where'->>'id'))::bigint) 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,14 +105,7 @@ 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) {
|
||||||
|
@ -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,14 +133,8 @@ 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) {
|
||||||
|
@ -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"= ((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_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,14 +164,7 @@ 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) {
|
||||||
|
@ -238,10 +180,6 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql1 := `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") = 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), "_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") = 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,16 +190,7 @@ 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) {
|
||||||
|
@ -272,9 +201,6 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
|
||||||
user_id
|
user_id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `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") = 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,14 +211,7 @@ 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 {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func nestedUpdateOneToOneWithDisconnectArray(t *testing.T) {
|
// func nestedUpdateOneToOneWithDisconnectArray(t *testing.T) {
|
||||||
|
|
|
@ -556,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)
|
||||||
}
|
}
|
||||||
|
|
169
qcode/qcode.go
169
qcode/qcode.go
|
@ -84,9 +84,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 string
|
||||||
NoLimit bool
|
NoLimit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +193,14 @@ func NewCompiler(c Config) (*Compiler, error) {
|
||||||
return co, nil
|
return co, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddFilter(sel *Select) *Exp {
|
||||||
|
ex := expPool.Get().(*Exp)
|
||||||
|
ex.Reset()
|
||||||
|
addFilter(sel, ex)
|
||||||
|
|
||||||
|
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{}
|
||||||
|
@ -400,10 +418,6 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
||||||
} else if role == "anon" {
|
} else if role == "anon" {
|
||||||
// Tables not defined under the anon role will not be rendered
|
// Tables not defined under the anon role will not be rendered
|
||||||
sel.SkipRender = true
|
sel.SkipRender = true
|
||||||
return
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if fil == nil {
|
if fil == nil {
|
||||||
|
@ -418,55 +432,58 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
||||||
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, role string) 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, role)
|
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 {
|
||||||
|
@ -529,7 +546,7 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
||||||
var needsUser bool
|
var needsUser bool
|
||||||
|
|
||||||
if node == nil || len(node.Children) == 0 {
|
if node == nil || len(node.Children) == 0 {
|
||||||
return nil, needsUser, errors.New("invalid argument value")
|
return nil, false, errors.New("invalid argument value")
|
||||||
}
|
}
|
||||||
|
|
||||||
pushChild(st, nil, node)
|
pushChild(st, nil, node)
|
||||||
|
@ -540,6 +557,7 @@ 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, needsUser, fmt.Errorf("16: unexpected value %v (%t)", intf, intf)
|
return nil, needsUser, fmt.Errorf("16: unexpected value %v (%t)", intf, intf)
|
||||||
|
@ -576,19 +594,23 @@ 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, needsUser, nil
|
return root, needsUser, nil
|
||||||
|
@ -644,19 +666,7 @@ func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -672,21 +682,9 @@ func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) (error,
|
||||||
if nu && role == "anon" {
|
if nu && role == "anon" {
|
||||||
sel.SkipRender = true
|
sel.SkipRender = true
|
||||||
}
|
}
|
||||||
|
addFilter(sel, ex)
|
||||||
|
|
||||||
if sel.Where != nil {
|
return nil, true
|
||||||
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, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
||||||
|
@ -713,7 +711,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -721,7 +719,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
||||||
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, 3)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -746,7 +744,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, 4)
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
@ -768,8 +766,9 @@ 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)
|
||||||
}
|
}
|
||||||
|
//FreeNode(node, 5)
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
@ -797,6 +796,32 @@ func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) {
|
||||||
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 fmt.Errorf("expecting an integer"), 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 != NodeStr {
|
||||||
|
return fmt.Errorf("expecting a string"), false
|
||||||
|
}
|
||||||
|
|
||||||
|
sel.Paging.Type = pt
|
||||||
|
sel.Paging.Cursor = node.Val
|
||||||
|
|
||||||
|
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 {
|
||||||
|
@ -807,6 +832,22 @@ func (com *Compiler) getRole(role, field string) *trval {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addFilter(sel *Select, fil *Exp) {
|
||||||
|
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] = fil
|
||||||
|
sel.Where.Children[1] = ow
|
||||||
|
} 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] == '_' {
|
||||||
|
@ -821,6 +862,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 {
|
||||||
|
@ -997,7 +1039,6 @@ 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, bool, error) {
|
func compileFilter(filter []string) (*Exp, bool, error) {
|
||||||
|
|
|
@ -38,6 +38,8 @@ var (
|
||||||
allowList *allow.List // allow.list is contains queries allowed in production
|
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 Cmd() {
|
func Cmd() {
|
||||||
|
|
|
@ -19,6 +19,7 @@ func cmdServ(cmd *cobra.Command, args []string) {
|
||||||
fatalInProd(err, "failed to connect to database")
|
fatalInProd(err, "failed to connect to database")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initCrypto()
|
||||||
initCompiler()
|
initCompiler()
|
||||||
initResolvers()
|
initResolvers()
|
||||||
initAllowList(confPath)
|
initAllowList(confPath)
|
||||||
|
|
|
@ -30,6 +30,7 @@ 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
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/cespare/xxhash/v2"
|
"github.com/cespare/xxhash/v2"
|
||||||
"github.com/dosco/super-graph/allow"
|
"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"
|
||||||
)
|
)
|
||||||
|
@ -241,6 +242,10 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if root, err = encryptCursor(st.qc, root); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if allowList.IsPersist() {
|
if allowList.IsPersist() {
|
||||||
if err := allowList.Set(c.req.Vars, c.req.Query, c.req.ref); err != nil {
|
if err := allowList.Set(c.req.Vars, c.req.Query, c.req.ref); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
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] < '0' || f.Value[0] > '9' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := crypto.Encrypt(f.Value, &internalKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteByte('"')
|
||||||
|
buf.WriteString(base64.StdEncoding.EncodeToString(v))
|
||||||
|
buf.WriteByte('"')
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
13
serv/init.go
13
serv/init.go
|
@ -2,10 +2,12 @@ package serv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/dosco/super-graph/allow"
|
"github.com/dosco/super-graph/allow"
|
||||||
|
"github.com/dosco/super-graph/crypto"
|
||||||
"github.com/jackc/pgx/v4"
|
"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"
|
||||||
|
@ -163,3 +165,14 @@ func initAllowList(cpath string) {
|
||||||
errlog.Fatal().Err(err).Msg("failed to initialize allow list")
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
||||||
|
|
||||||
pc := psql.NewCompiler(psql.Config{
|
pc := psql.NewCompiler(psql.Config{
|
||||||
Schema: schema,
|
Schema: schema,
|
||||||
|
Decryptor: decrypt,
|
||||||
Vars: c.DB.Vars,
|
Vars: c.DB.Vars,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue