Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
e102da839e | |||
68a378c00f | |||
d96eaf14f4 | |||
01e488b69d | |||
7a450b16ba | |||
1ad8cbf15b | |||
f69f1c67d5 | |||
a172193955 | |||
81338b6123 | |||
265b93b203 | |||
6c240e21b4 | |||
7930719eaa | |||
cc687b1b2b | |||
3033dcf1a9 | |||
0381982d19 | |||
2b0a798faa | |||
8b6c562ac1 | |||
a1fb89b762 | |||
c82a7bff0d | |||
7acf28bb3c | |||
be5d4e976a | |||
d1b884aec6 | |||
4be4ce860b | |||
dfa4caf540 | |||
7763251fb7 | |||
51e105699e | |||
90694f8803 | |||
ad82f5b267 | |||
99b37a9c50 | |||
7ec1f59224 | |||
d3ecb1d6cc | |||
aed4170e8e | |||
c33e93ab37 | |||
3d3e5d9c2b | |||
67b4a4d945 | |||
7413813138 | |||
12007db76e | |||
c85d379fe2 |
16
Dockerfile
16
Dockerfile
@ -6,14 +6,19 @@ RUN yarn
|
|||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
# stage: 2
|
# stage: 2
|
||||||
FROM golang:1.13.4-alpine as go-build
|
FROM golang:1.14-alpine as go-build
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache make && \
|
apk add --no-cache make && \
|
||||||
apk add --no-cache git && \
|
apk add --no-cache git && \
|
||||||
|
apk add --no-cache jq && \
|
||||||
apk add --no-cache upx=3.95-r2
|
apk add --no-cache upx=3.95-r2
|
||||||
|
|
||||||
RUN GO111MODULE=off go get -u github.com/rafaelsq/wtc
|
RUN GO111MODULE=off go get -u github.com/rafaelsq/wtc
|
||||||
|
|
||||||
|
ARG SOPS_VERSION=3.5.0
|
||||||
|
ADD https://github.com/mozilla/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux /usr/local/bin/sops
|
||||||
|
RUN chmod 755 /usr/local/bin/sops
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
@ -36,10 +41,15 @@ RUN mkdir -p /config
|
|||||||
COPY --from=go-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=go-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=go-build /app/config/* /config/
|
COPY --from=go-build /app/config/* /config/
|
||||||
COPY --from=go-build /app/super-graph .
|
COPY --from=go-build /app/super-graph .
|
||||||
|
COPY --from=go-build /app/scripts/start.sh .
|
||||||
|
COPY --from=go-build /usr/local/bin/sops .
|
||||||
|
|
||||||
RUN chmod +x /super-graph
|
RUN chmod +x /super-graph
|
||||||
|
RUN chmod +x /start.sh
|
||||||
|
|
||||||
USER nobody
|
USER nobody
|
||||||
|
|
||||||
EXPOSE 8080
|
ENV GO_ENV production
|
||||||
|
|
||||||
CMD ./super-graph serv
|
ENTRYPOINT ["./start.sh"]
|
||||||
|
CMD ["./super-graph", "serv"]
|
||||||
|
4
Makefile
4
Makefile
@ -35,7 +35,7 @@ $(GORICE):
|
|||||||
|
|
||||||
$(WEB_BUILD_DIR):
|
$(WEB_BUILD_DIR):
|
||||||
@echo "First install Yarn and create a build of the web UI found under ./web"
|
@echo "First install Yarn and create a build of the web UI found under ./web"
|
||||||
@echo "Command: cd web && yarn build"
|
@echo "Command: cd web && yarn && yarn build"
|
||||||
@exit 1
|
@exit 1
|
||||||
|
|
||||||
$(GITCHGLOG):
|
$(GITCHGLOG):
|
||||||
@ -77,7 +77,7 @@ clean:
|
|||||||
run: clean
|
run: clean
|
||||||
@go run $(BUILD_FLAGS) main.go $(ARGS)
|
@go run $(BUILD_FLAGS) main.go $(ARGS)
|
||||||
|
|
||||||
install:
|
install: gen
|
||||||
@echo $(GOPATH)
|
@echo $(GOPATH)
|
||||||
@echo "Commit Hash: `git rev-parse HEAD`"
|
@echo "Commit Hash: `git rev-parse HEAD`"
|
||||||
@echo "Old Hash: `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`"
|
@echo "Old Hash: `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`"
|
||||||
|
@ -48,6 +48,7 @@ This compiler is what sits at the heart of Super Graph with layers of useful fun
|
|||||||
- Fuzz tested for security
|
- Fuzz tested for security
|
||||||
- Database migrations tool
|
- Database migrations tool
|
||||||
- Database seeding tool
|
- Database seeding tool
|
||||||
|
- Works with Postgres and YugabyteDB
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
|
@ -18,11 +18,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Name string
|
Name string
|
||||||
key string
|
key string
|
||||||
URI string
|
Query string
|
||||||
Query string
|
Vars json.RawMessage
|
||||||
Vars json.RawMessage
|
Comment string
|
||||||
}
|
}
|
||||||
|
|
||||||
type List struct {
|
type List struct {
|
||||||
@ -105,7 +105,7 @@ func (al *List) IsPersist() bool {
|
|||||||
return al.saveChan != nil
|
return al.saveChan != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *List) Add(vars []byte, query, uri string) error {
|
func (al *List) Set(vars []byte, query, comment string) error {
|
||||||
if al.saveChan == nil {
|
if al.saveChan == nil {
|
||||||
return errors.New("allow.list is read-only")
|
return errors.New("allow.list is read-only")
|
||||||
}
|
}
|
||||||
@ -129,9 +129,9 @@ func (al *List) Add(vars []byte, query, uri string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
al.saveChan <- Item{
|
al.saveChan <- Item{
|
||||||
URI: uri,
|
Comment: comment,
|
||||||
Query: q,
|
Query: q,
|
||||||
Vars: vars,
|
Vars: vars,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -149,7 +149,7 @@ func (al *List) Load() ([]Item, error) {
|
|||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var uri string
|
var comment bytes.Buffer
|
||||||
var varBytes []byte
|
var varBytes []byte
|
||||||
|
|
||||||
itemMap := make(map[string]struct{})
|
itemMap := make(map[string]struct{})
|
||||||
@ -166,7 +166,7 @@ func (al *List) Load() ([]Item, error) {
|
|||||||
e++
|
e++
|
||||||
}
|
}
|
||||||
if (e - s) > 2 {
|
if (e - s) > 2 {
|
||||||
uri = strings.TrimSpace(string(b[(s + 1):e]))
|
comment.Write(b[(s + 1):(e + 1)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,13 +207,14 @@ func (al *List) Load() ([]Item, error) {
|
|||||||
|
|
||||||
if _, ok := itemMap[key]; !ok {
|
if _, ok := itemMap[key]; !ok {
|
||||||
v := Item{
|
v := Item{
|
||||||
Name: name,
|
Name: name,
|
||||||
key: key,
|
key: key,
|
||||||
URI: uri,
|
Query: query,
|
||||||
Query: query,
|
Vars: varBytes,
|
||||||
Vars: varBytes,
|
Comment: comment.String(),
|
||||||
}
|
}
|
||||||
list = append(list, v)
|
list = append(list, v)
|
||||||
|
comment.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
varBytes = nil
|
varBytes = nil
|
||||||
@ -252,6 +253,9 @@ func (al *List) save(item Item) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if index != -1 {
|
if index != -1 {
|
||||||
|
if len(list[index].Comment) != 0 {
|
||||||
|
item.Comment = list[index].Comment
|
||||||
|
}
|
||||||
list[index] = item
|
list[index] = item
|
||||||
} else {
|
} else {
|
||||||
list = append(list, item)
|
list = append(list, item)
|
||||||
@ -269,9 +273,29 @@ func (al *List) save(item Item) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, v := range list {
|
for _, v := range list {
|
||||||
_, err := f.WriteString(fmt.Sprintf("# %s\n\n", v.URI))
|
cmtLines := strings.Split(v.Comment, "\n")
|
||||||
if err != nil {
|
|
||||||
return err
|
i := 0
|
||||||
|
for _, c := range cmtLines {
|
||||||
|
if c = strings.TrimSpace(c); len(c) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := f.WriteString(fmt.Sprintf("# %s\n", c))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != 0 {
|
||||||
|
if _, err := f.WriteString("\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := f.WriteString(fmt.Sprintf("# Query named %s\n\n", v.Name)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v.Vars) != 0 && !bytes.Equal(v.Vars, []byte("{}")) {
|
if len(v.Vars) != 0 && !bytes.Equal(v.Vars, []byte("{}")) {
|
||||||
@ -317,17 +341,13 @@ func QueryName(b string) string {
|
|||||||
|
|
||||||
for i := 0; i < len(b); i++ {
|
for i := 0; i < len(b); i++ {
|
||||||
switch {
|
switch {
|
||||||
case state == 2 && b[i] == '{':
|
case state == 2 && !isValidNameChar(b[i]):
|
||||||
return b[s:i]
|
|
||||||
case state == 2 && b[i] == ' ':
|
|
||||||
return b[s:i]
|
return b[s:i]
|
||||||
case state == 1 && b[i] == '{':
|
case state == 1 && b[i] == '{':
|
||||||
return ""
|
return ""
|
||||||
case state == 1 && b[i] != ' ':
|
case state == 1 && isValidNameChar(b[i]):
|
||||||
s = i
|
s = i
|
||||||
state = 2
|
state = 2
|
||||||
case state == 1 && b[i] == ' ':
|
|
||||||
continue
|
|
||||||
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
|
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
|
||||||
state = 1
|
state = 1
|
||||||
}
|
}
|
||||||
@ -335,3 +355,7 @@ func QueryName(b string) string {
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidNameChar(c byte) bool {
|
||||||
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
|
||||||
|
}
|
||||||
|
@ -21,7 +21,9 @@ func TestGQLName1(t *testing.T) {
|
|||||||
|
|
||||||
func TestGQLName2(t *testing.T) {
|
func TestGQLName2(t *testing.T) {
|
||||||
var q = `
|
var q = `
|
||||||
query hakuna_matata {
|
query hakuna_matata
|
||||||
|
|
||||||
|
{
|
||||||
products(
|
products(
|
||||||
distinct: [price]
|
distinct: [price]
|
||||||
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
|
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
|
||||||
|
@ -32,6 +32,19 @@ 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
|
||||||
|
|
||||||
|
# CORS: A list of origins a cross-domain request can be executed from.
|
||||||
|
# If the special * value is present in the list, all origins will be allowed.
|
||||||
|
# An origin may contain a wildcard (*) to replace 0 or more
|
||||||
|
# characters (i.e.: http://*.domain.com).
|
||||||
|
cors_allowed_origins: ["*"]
|
||||||
|
|
||||||
|
# Debug Cross Origin Resource Sharing requests
|
||||||
|
cors_debug: true
|
||||||
|
|
||||||
# Postgres related environment Variables
|
# Postgres related environment Variables
|
||||||
# SG_DATABASE_HOST
|
# SG_DATABASE_HOST
|
||||||
# SG_DATABASE_PORT
|
# SG_DATABASE_PORT
|
||||||
@ -89,7 +102,7 @@ database:
|
|||||||
port: 5432
|
port: 5432
|
||||||
dbname: app_development
|
dbname: app_development
|
||||||
user: postgres
|
user: postgres
|
||||||
password: ''
|
password: postgres
|
||||||
|
|
||||||
#schema: "public"
|
#schema: "public"
|
||||||
#pool_size: 10
|
#pool_size: 10
|
||||||
|
@ -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
|
||||||
@ -50,7 +54,7 @@ database:
|
|||||||
port: 5432
|
port: 5432
|
||||||
dbname: app_production
|
dbname: app_production
|
||||||
user: postgres
|
user: postgres
|
||||||
password: ''
|
password: postgres
|
||||||
#pool_size: 10
|
#pool_size: 10
|
||||||
#max_retries: 0
|
#max_retries: 0
|
||||||
#log_level: "debug"
|
#log_level: "debug"
|
||||||
|
@ -11,7 +11,7 @@ for (i = 0; i < user_count; i++) {
|
|||||||
var pwd = fake.password()
|
var pwd = fake.password()
|
||||||
var data = {
|
var data = {
|
||||||
full_name: fake.name(),
|
full_name: fake.name(),
|
||||||
avatar: fake.image_url(),
|
avatar: fake.avatar_url(200),
|
||||||
phone: fake.phone(),
|
phone: fake.phone(),
|
||||||
email: fake.email(),
|
email: fake.email(),
|
||||||
password: pwd,
|
password: pwd,
|
||||||
|
80
crypto/encrypt.go
Normal file
80
crypto/encrypt.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// cryptopasta - basic cryptography examples
|
||||||
|
//
|
||||||
|
// Written in 2015 by George Tankersley <george.tankersley@gmail.com>
|
||||||
|
//
|
||||||
|
// To the extent possible under law, the author(s) have dedicated all copyright
|
||||||
|
// and related and neighboring rights to this software to the public domain
|
||||||
|
// worldwide. This software is distributed without any warranty.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the CC0 Public Domain Dedication along
|
||||||
|
// with this software. If not, see // <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||||
|
|
||||||
|
// Provides symmetric authenticated encryption using 256-bit AES-GCM with a random nonce.
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewEncryptionKey generates a random 256-bit key for Encrypt() and
|
||||||
|
// Decrypt(). It panics if the source of randomness fails.
|
||||||
|
func NewEncryptionKey() [32]byte {
|
||||||
|
key := [32]byte{}
|
||||||
|
_, err := io.ReadFull(rand.Reader, key[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of
|
||||||
|
// the data and provides a check that it hasn't been altered. Output takes the
|
||||||
|
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
||||||
|
func Encrypt(plaintext []byte, key *[32]byte) (ciphertext []byte, err error) {
|
||||||
|
block, err := aes.NewCipher(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
_, err = io.ReadFull(rand.Reader, nonce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of
|
||||||
|
// the data and provides a check that it hasn't been altered. Expects input
|
||||||
|
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
||||||
|
func Decrypt(ciphertext []byte, key *[32]byte) (plaintext []byte, err error) {
|
||||||
|
block, err := aes.NewCipher(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ciphertext) < gcm.NonceSize() {
|
||||||
|
return nil, errors.New("malformed ciphertext")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcm.Open(nil,
|
||||||
|
ciphertext[:gcm.NonceSize()],
|
||||||
|
ciphertext[gcm.NonceSize():],
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
@ -1,7 +1,10 @@
|
|||||||
version: '3.4'
|
version: '3.4'
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres
|
image: postgres:12
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
|
||||||
|
200
docs/guide.md
200
docs/guide.md
@ -34,6 +34,12 @@ Super Graph has a rich feature set like integrating with your existing Ruby on R
|
|||||||
# clone the repository
|
# clone the repository
|
||||||
git clone https://github.com/dosco/super-graph
|
git clone https://github.com/dosco/super-graph
|
||||||
|
|
||||||
|
# run db in background
|
||||||
|
docker-compose up -d db
|
||||||
|
|
||||||
|
# see logs and wait until DB is really UP
|
||||||
|
docker-compose logs db
|
||||||
|
|
||||||
# setup the demo rails app & database and run it
|
# setup the demo rails app & database and run it
|
||||||
docker-compose run rails_app rake db:create db:migrate db:seed
|
docker-compose run rails_app rake db:create db:migrate db:seed
|
||||||
|
|
||||||
@ -137,7 +143,7 @@ What if I told you Super Graph will fetch all this data with a single SQL query
|
|||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
query {
|
query {
|
||||||
products(limit 5, where: { price: { gt: 12 } }) {
|
products(limit: 5, where: { price: { gt: 12 } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
@ -153,7 +159,7 @@ query {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
purchases(
|
purchases(
|
||||||
limit 10,
|
limit: 10,
|
||||||
order_by: { created_at: desc } ,
|
order_by: { created_at: desc } ,
|
||||||
where: { user_id: { eq: $user_id } }
|
where: { user_id: { eq: $user_id } }
|
||||||
) {
|
) {
|
||||||
@ -216,7 +222,7 @@ You can then add your database schema to the migrations, maybe create some seed
|
|||||||
git clone https://github.com/dosco/super-graph && cd super-graph && make install
|
git clone https://github.com/dosco/super-graph && cd super-graph && make install
|
||||||
```
|
```
|
||||||
|
|
||||||
And then create and launch you're new app
|
And then create and launch your new app
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# create a new app and change to it's directory
|
# create a new app and change to it's directory
|
||||||
@ -286,6 +292,12 @@ for (i = 0; i < 10; i++) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to import a lot of data using a CSV file is the best and fastest option. The `import_csv` command uses the `COPY FROM` Postgres method to load massive amounts of data into tables. The first line of the CSV file must be the header with column names.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var post_count = import_csv("posts", "posts.csv")
|
||||||
|
```
|
||||||
|
|
||||||
You can generate the following fake data for your seeding purposes. Below is the list of fake data functions supported by the built-in fake data library. For example `fake.image_url()` will generate a fake image url or `fake.shuffle_strings(['hello', 'world', 'cool'])` will generate a randomly shuffled version of that array of strings or `fake.rand_string(['hello', 'world', 'cool'])` will return a random string from the array provided.
|
You can generate the following fake data for your seeding purposes. Below is the list of fake data functions supported by the built-in fake data library. For example `fake.image_url()` will generate a fake image url or `fake.shuffle_strings(['hello', 'world', 'cool'])` will generate a randomly shuffled version of that array of strings or `fake.rand_string(['hello', 'world', 'cool'])` will return a random string from the array provided.
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -1024,9 +1036,116 @@ 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
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"variables": {
|
||||||
|
"cursor": "MJoTLbQF4l0GuoDsYmCrpjPeaaIlNpfm4uFU4PQ="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products(first: 10, after: $cursor) {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Paginating backward through your results
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products(last: 10, before: $cursor) {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
"data": {
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"slug": "eius-nulla-et-8",
|
||||||
|
"name" "Pale Ale"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "sapiente-ut-alias-12",
|
||||||
|
"name" "Brown Ale"
|
||||||
|
}
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"products_cursor": "dJwHassm5+d82rGydH2xQnwNxJ1dcj4/cxkh5Cer"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested tables can also have cursors. Requesting multiple cursors are supported on a single request but when paginating using a cursor only one table is currently supported. To explain this better, you can only use a `before` or `after` argument with a cursor value to paginate a single table in a query.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products(last: 10) {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
customers(last: 5) {
|
||||||
|
email
|
||||||
|
full_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple order-by arguments are supported. Super Graph is smart enough to allow cursor based pagination when you also need complex sort order like below.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products(
|
||||||
|
last: 10
|
||||||
|
before: $cursor
|
||||||
|
order_by: [ price: desc, total_customers: asc ]) {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Using Variables
|
## 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 built-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner
|
||||||
and better client side code.
|
and better client side code.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -1445,7 +1564,7 @@ roles:
|
|||||||
|
|
||||||
This configuration is relatively simple to follow the `roles_query` parameter is the query that must be run to help figure out a users role. This query can be as complex as you like and include joins with other tables.
|
This configuration is relatively simple to follow the `roles_query` parameter is the query that must be run to help figure out a users role. This query can be as complex as you like and include joins with other tables.
|
||||||
|
|
||||||
The individual roles are defined under the `roles` parameter and this includes each table the role has a custom setting for. The role is dynamically matched using the `match` parameter for example in the above case `users.id = 1` means that when the `roles_query` is executed a user with the id `1` willbe assigned the admin role and those that don't match get the `user` role if authenticated successfully or the `anon` role.
|
The individual roles are defined under the `roles` parameter and this includes each table the role has a custom setting for. The role is dynamically matched using the `match` parameter for example in the above case `users.id = 1` means that when the `roles_query` is executed a user with the id `1` will be assigned the admin role and those that don't match get the `user` role if authenticated successfully or the `anon` role.
|
||||||
|
|
||||||
## Remote Joins
|
## Remote Joins
|
||||||
|
|
||||||
@ -1542,7 +1661,7 @@ tables:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Configuration files
|
## Configuration
|
||||||
|
|
||||||
Configuration files can either be in YAML or JSON their names are derived from the `GO_ENV` variable, for example `GO_ENV=prod` will cause the `prod.yaml` config file to be used. or `GO_ENV=dev` will use the `dev.yaml`. A path to look for the config files in can be specified using the `-path <folder>` command line argument.
|
Configuration files can either be in YAML or JSON their names are derived from the `GO_ENV` variable, for example `GO_ENV=prod` will cause the `prod.yaml` config file to be used. or `GO_ENV=dev` will use the `dev.yaml`. A path to look for the config files in can be specified using the `-path <folder>` command line argument.
|
||||||
|
|
||||||
@ -1660,7 +1779,7 @@ database:
|
|||||||
port: 5432
|
port: 5432
|
||||||
dbname: app_development
|
dbname: app_development
|
||||||
user: postgres
|
user: postgres
|
||||||
password: ''
|
password: postgres
|
||||||
|
|
||||||
#schema: "public"
|
#schema: "public"
|
||||||
#pool_size: 10
|
#pool_size: 10
|
||||||
@ -1795,9 +1914,74 @@ SG_AUTH_RAILS_REDIS_PASSWORD
|
|||||||
SG_AUTH_JWT_PUBLIC_KEY_FILE
|
SG_AUTH_JWT_PUBLIC_KEY_FILE
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## YugabyteDB
|
||||||
|
|
||||||
|
Yugabyte is an open-source, geo-distrubuted cloud-native relational DB that scales horizontally. Super Graph works with Yugabyte right out of the box. If you think you're data needs will outgrow Postgres and you don't really want to deal with sharding then Yugabyte is the way to go. Just point Super Graph to your Yugabyte DB and everything will just work including running migrations, seeding, querying, mutations, etc.
|
||||||
|
|
||||||
|
To use Yugabyte in your local development flow just uncomment the following lines in the `docker-compose.yml` file that is part of your Super Graph app. Also remember to comment out the originl postgres `db` config.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Postgres DB
|
||||||
|
# db:
|
||||||
|
# image: postgres:latest
|
||||||
|
# ports:
|
||||||
|
# - "5432:5432"
|
||||||
|
|
||||||
|
#Standard config to run a single node of Yugabyte
|
||||||
|
yb-master:
|
||||||
|
image: yugabytedb/yugabyte:latest
|
||||||
|
container_name: yb-master-n1
|
||||||
|
command: [ "/home/yugabyte/bin/yb-master",
|
||||||
|
"--fs_data_dirs=/mnt/disk0,/mnt/disk1",
|
||||||
|
"--master_addresses=yb-master-n1:7100",
|
||||||
|
"--replication_factor=1",
|
||||||
|
"--enable_ysql=true"]
|
||||||
|
ports:
|
||||||
|
- "7000:7000"
|
||||||
|
environment:
|
||||||
|
SERVICE_7000_NAME: yb-master
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: yugabytedb/yugabyte:latest
|
||||||
|
container_name: yb-tserver-n1
|
||||||
|
command: [ "/home/yugabyte/bin/yb-tserver",
|
||||||
|
"--fs_data_dirs=/mnt/disk0,/mnt/disk1",
|
||||||
|
"--start_pgsql_proxy",
|
||||||
|
"--tserver_master_addrs=yb-master-n1:7100"]
|
||||||
|
ports:
|
||||||
|
- "9042:9042"
|
||||||
|
- "6379:6379"
|
||||||
|
- "5433:5433"
|
||||||
|
- "9000:9000"
|
||||||
|
environment:
|
||||||
|
SERVICE_5433_NAME: ysql
|
||||||
|
SERVICE_9042_NAME: ycql
|
||||||
|
SERVICE_6379_NAME: yedis
|
||||||
|
SERVICE_9000_NAME: yb-tserver
|
||||||
|
depends_on:
|
||||||
|
- yb-master
|
||||||
|
|
||||||
|
# Environment variables to point Super Graph to Yugabyte
|
||||||
|
# This is required since it uses a different user and port number
|
||||||
|
yourapp_api:
|
||||||
|
image: dosco/super-graph:latest
|
||||||
|
environment:
|
||||||
|
GO_ENV: "development"
|
||||||
|
Uncomment below for Yugabyte DB
|
||||||
|
SG_DATABASE_PORT: 5433
|
||||||
|
SG_DATABASE_USER: yugabyte
|
||||||
|
SG_DATABASE_PASSWORD: yugabyte
|
||||||
|
volumes:
|
||||||
|
- ./config:/config
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
```
|
||||||
|
|
||||||
## Developing Super Graph
|
## Developing Super Graph
|
||||||
|
|
||||||
If you want to build and run Super Graph from code then the below commands will build the web ui and launch Super Graph in developer mode with a watcher to rebuild on code changes. And the demo rails app is also launched to make it essier to test changes.
|
If you want to build and run Super Graph from code then the below commands will build the web ui and launch Super Graph in developer mode with a watcher to rebuild on code changes. And the demo rails app is also launched to make it easier to test changes.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ Super Graph code is made up of a number of packages. We have done our best to ke
|
|||||||
|
|
||||||
## QCODE
|
## QCODE
|
||||||
|
|
||||||
This package contains the core of the GraphQL conpiler it handling the lexing and parsing of the GraphQL query transforming it into an internal representation called
|
This package contains the core of the GraphQL compiler it handling the lexing and parsing of the GraphQL query transforming it into an internal representation called
|
||||||
`QCode`.
|
`QCode`.
|
||||||
|
|
||||||
This is the first step of the compiling process the `func NewCompiler(c Config)` function creates a new instance of this compiler which has it's own config.
|
This is the first step of the compiling process the `func NewCompiler(c Config)` function creates a new instance of this compiler which has it's own config.
|
||||||
@ -71,7 +71,7 @@ item{itemObjOpen, 16, 20} // {
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
These tokens are then fed into the parser `parse.go` the parser does the work of generating an abstract syntax tree (AST) from the tokens. This AST is an internal representation (data structure) and is not exposed outside the package. Sinc the AST is a tree a stack `stack.go` is used to walk the tree and generate the QCode AST. The QCode data structure is also a tree (represented as an array). This is then returned to the caller of the compile function.
|
These tokens are then fed into the parser `parse.go` the parser does the work of generating an abstract syntax tree (AST) from the tokens. This AST is an internal representation (data structure) and is not exposed outside the package. Since the AST is a tree a stack `stack.go` is used to walk the tree and generate the QCode AST. The QCode data structure is also a tree (represented as an array). This is then returned to the caller of the compile function.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Operation struct {
|
type Operation struct {
|
||||||
@ -238,4 +238,4 @@ ok github.com/dosco/super-graph/psql 2.530s
|
|||||||
|
|
||||||
## Reach out
|
## Reach out
|
||||||
|
|
||||||
If you'd like me to explain other parts of the code please reach out over Twitter or Discord. I'll keep adding to this doc as I get time.
|
If you'd like me to explain other parts of the code please reach out over Twitter or Discord. I'll keep adding to this doc as I get time.
|
||||||
|
@ -19,7 +19,7 @@ default: &default
|
|||||||
encoding: unicode
|
encoding: unicode
|
||||||
host: db
|
host: db
|
||||||
username: postgres
|
username: postgres
|
||||||
password:
|
password: postgres
|
||||||
pool: 5
|
pool: 5
|
||||||
|
|
||||||
development:
|
development:
|
||||||
|
3
go.mod
3
go.mod
@ -2,7 +2,6 @@ module github.com/dosco/super-graph
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/GeertJohan/go.rice v1.0.0
|
github.com/GeertJohan/go.rice v1.0.0
|
||||||
github.com/Masterminds/semver v1.5.0
|
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3
|
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||||
@ -12,7 +11,6 @@ require (
|
|||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/dlclark/regexp2 v1.2.0 // indirect
|
github.com/dlclark/regexp2 v1.2.0 // indirect
|
||||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733
|
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733
|
||||||
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7
|
github.com/fsnotify/fsnotify v1.4.7
|
||||||
github.com/garyburd/redigo v1.6.0
|
github.com/garyburd/redigo v1.6.0
|
||||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
|
||||||
@ -23,6 +21,7 @@ require (
|
|||||||
github.com/magiconair/properties v1.8.1 // indirect
|
github.com/magiconair/properties v1.8.1 // indirect
|
||||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
|
github.com/rs/cors v1.7.0
|
||||||
github.com/rs/zerolog v1.15.0
|
github.com/rs/zerolog v1.15.0
|
||||||
github.com/spf13/afero v1.2.2 // indirect
|
github.com/spf13/afero v1.2.2 // indirect
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v0.0.5
|
||||||
|
6
go.sum
6
go.sum
@ -5,8 +5,6 @@ github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/
|
|||||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||||
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
|
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
|
||||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
|
||||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
|
||||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
@ -54,8 +52,6 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
|
|||||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw=
|
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw=
|
||||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
||||||
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c h1:/bXaeEuNG6V0HeyEGw11DYLW5BGsOPlcVRIXbHNUWSo=
|
|
||||||
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||||
@ -184,6 +180,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
|
|||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||||
|
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
|
github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
|
||||||
|
@ -27,14 +27,27 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
|||||||
|
|
||||||
var k []byte
|
var k []byte
|
||||||
state := expectKey
|
state := expectKey
|
||||||
|
instr := false
|
||||||
|
slash := 0
|
||||||
|
|
||||||
for i := 0; i < len(b); i++ {
|
for i := 0; i < len(b); i++ {
|
||||||
|
if instr && b[i] == '\\' {
|
||||||
|
slash++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b[i] == '"' && (slash%2 == 0) {
|
||||||
|
instr = !instr
|
||||||
|
}
|
||||||
|
|
||||||
if state == expectObjClose || state == expectListClose {
|
if state == expectObjClose || state == expectListClose {
|
||||||
switch b[i] {
|
if !instr {
|
||||||
case '{', '[':
|
switch b[i] {
|
||||||
d++
|
case '{', '[':
|
||||||
case '}', ']':
|
d++
|
||||||
d--
|
case '}', ']':
|
||||||
|
d--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +77,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectKeyClose && (b[i] == '"' && (slash%2 == 0)):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
k = b[(s + 1):i]
|
k = b[(s + 1):i]
|
||||||
|
|
||||||
@ -74,7 +87,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
|||||||
case state == expectValue && b[i] == '"':
|
case state == expectValue && b[i] == '"':
|
||||||
state = expectString
|
state = expectString
|
||||||
|
|
||||||
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectString && (b[i] == '"' && (slash%2 == 0)):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '[':
|
case state == expectValue && b[i] == '[':
|
||||||
@ -109,7 +122,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
33
jsn/get.go
33
jsn/get.go
@ -51,13 +51,27 @@ func Get(b []byte, keys [][]byte) []Field {
|
|||||||
state := expectKey
|
state := expectKey
|
||||||
|
|
||||||
n := 0
|
n := 0
|
||||||
|
instr := false
|
||||||
|
slash := 0
|
||||||
|
|
||||||
for i := 0; i < len(b); i++ {
|
for i := 0; i < len(b); i++ {
|
||||||
|
if instr && b[i] == '\\' {
|
||||||
|
slash++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b[i] == '"' && (slash%2 == 0) {
|
||||||
|
instr = !instr
|
||||||
|
}
|
||||||
|
|
||||||
if state == expectObjClose || state == expectListClose {
|
if state == expectObjClose || state == expectListClose {
|
||||||
switch b[i] {
|
if !instr {
|
||||||
case '{', '[':
|
switch b[i] {
|
||||||
d++
|
case '{', '[':
|
||||||
case '}', ']':
|
d++
|
||||||
d--
|
case '}', ']':
|
||||||
|
d--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +80,7 @@ func Get(b []byte, keys [][]byte) []Field {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectKeyClose && (b[i] == '"' && (slash%2 == 0)):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
k = b[(s + 1):i]
|
k = b[(s + 1):i]
|
||||||
|
|
||||||
@ -77,7 +91,7 @@ func Get(b []byte, keys [][]byte) []Field {
|
|||||||
state = expectString
|
state = expectString
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectString && (b[i] == '"' && (slash%2 == 0)):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '[':
|
case state == expectValue && b[i] == '[':
|
||||||
@ -117,8 +131,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +162,8 @@ func Get(b []byte, keys [][]byte) []Field {
|
|||||||
state = expectKey
|
state = expectKey
|
||||||
e = 0
|
e = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slash = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return res[:n]
|
return res[:n]
|
||||||
|
@ -2,6 +2,9 @@ package jsn
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -158,6 +161,13 @@ 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}`
|
||||||
|
|
||||||
|
input7, _ = ioutil.ReadFile("test7.json")
|
||||||
|
|
||||||
|
input8, _ = ioutil.ReadFile("test8.json")
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
func TestGet(t *testing.T) {
|
||||||
@ -227,6 +237,58 @@ 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 TestGet3(t *testing.T) {
|
||||||
|
values := Get(input7, [][]byte{[]byte("data")})
|
||||||
|
v := values[0].Value
|
||||||
|
|
||||||
|
if !bytes.Equal(v[len(v)-11:], []byte(`Rangnekar"}`)) {
|
||||||
|
t.Fatal("corrupt ending")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet4(t *testing.T) {
|
||||||
|
exp := `"# \n\n@@@java\npackage main\n\nimport (\n \"net/http\"\n \"strings\"\n\n \"github.com/gin-gonic/gin\"\n)\n\nfunc main() {\n r := gin.Default()\n r.LoadHTMLGlob(\"templates/*\")\n\n r.GET(\"/\", handleIndex)\n r.GET(\"/to/:name\", handleIndex)\n r.Run()\n}\n\n// Hello is page data for the template\ntype Hello struct {\n Name string\n}\n\nfunc handleIndex(c *gin.Context) {\n name := c.Param(\"name\")\n if name != \"\" {\n name = strings.TrimPrefix(c.Param(\"name\"), \"/\")\n }\n c.HTML(http.StatusOK, \"hellofly.tmpl\", gin.H{\"Name\": name})\n}\n@@@\n\n\\"`
|
||||||
|
|
||||||
|
exp = strings.ReplaceAll(exp, "@", "`")
|
||||||
|
|
||||||
|
values := Get(input8, [][]byte{[]byte("body")})
|
||||||
|
|
||||||
|
if string(values[0].Key) != "body" {
|
||||||
|
t.Fatal("unexpected key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(values[0].Value) != exp {
|
||||||
|
fmt.Println(string(values[0].Value))
|
||||||
|
t.Fatal("unexpected 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) {
|
||||||
|
30
jsn/keys.go
30
jsn/keys.go
@ -10,15 +10,27 @@ func Keys(b []byte) [][]byte {
|
|||||||
|
|
||||||
st := NewStack()
|
st := NewStack()
|
||||||
ae := 0
|
ae := 0
|
||||||
|
instr := false
|
||||||
|
slash := 0
|
||||||
|
|
||||||
for i := 0; i < len(b); i++ {
|
for i := 0; i < len(b); i++ {
|
||||||
|
if instr && b[i] == '\\' {
|
||||||
|
slash++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b[i] == '"' && (slash%2 == 0) {
|
||||||
|
instr = !instr
|
||||||
|
}
|
||||||
|
|
||||||
if state == expectObjClose || state == expectListClose {
|
if state == expectObjClose || state == expectListClose {
|
||||||
switch b[i] {
|
if !instr {
|
||||||
case '{', '[':
|
switch b[i] {
|
||||||
d++
|
case '{', '[':
|
||||||
case '}', ']':
|
d++
|
||||||
d--
|
case '}', ']':
|
||||||
|
d--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +59,7 @@ func Keys(b []byte) [][]byte {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectKeyClose && (b[i] == '"' && (slash%2 == 0)):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
k = b[(s + 1):i]
|
k = b[(s + 1):i]
|
||||||
|
|
||||||
@ -58,7 +70,7 @@ func Keys(b []byte) [][]byte {
|
|||||||
state = expectString
|
state = expectString
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectString && (b[i] == '"' && (slash%2 == 0)):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '{':
|
case state == expectValue && b[i] == '{':
|
||||||
@ -101,8 +113,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +142,7 @@ func Keys(b []byte) [][]byte {
|
|||||||
e = 0
|
e = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slash = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
@ -12,6 +12,11 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
|||||||
return errors.New("'from' and 'to' must be of the same length")
|
return errors.New("'from' and 'to' must be of the same length")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(from) == 0 || len(to) == 0 {
|
||||||
|
_, err := w.Write(b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
h := xxhash.New()
|
h := xxhash.New()
|
||||||
tmap := make(map[uint64]int, len(from))
|
tmap := make(map[uint64]int, len(from))
|
||||||
|
|
||||||
@ -32,18 +37,32 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
|||||||
state := expectKey
|
state := expectKey
|
||||||
ws, we := -1, len(b)
|
ws, we := -1, len(b)
|
||||||
|
|
||||||
|
instr := false
|
||||||
|
slash := 0
|
||||||
|
|
||||||
for i := 0; i < len(b); i++ {
|
for i := 0; i < len(b); i++ {
|
||||||
|
if instr && b[i] == '\\' {
|
||||||
|
slash++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// skip any left padding whitespace
|
// skip any left padding whitespace
|
||||||
if ws == -1 && (b[i] == '{' || b[i] == '[') {
|
if ws == -1 && (b[i] == '{' || b[i] == '[') {
|
||||||
ws = i
|
ws = i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b[i] == '"' && (slash%2 == 0) {
|
||||||
|
instr = !instr
|
||||||
|
}
|
||||||
|
|
||||||
if state == expectObjClose || state == expectListClose {
|
if state == expectObjClose || state == expectListClose {
|
||||||
switch b[i] {
|
if !instr {
|
||||||
case '{', '[':
|
switch b[i] {
|
||||||
d++
|
case '{', '[':
|
||||||
case '}', ']':
|
d++
|
||||||
d--
|
case '}', ']':
|
||||||
|
d--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +71,7 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectKeyClose && (b[i] == '"' && (slash%2 == 0)):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
if _, err := h.Write(b[(s + 1):i]); err != nil {
|
if _, err := h.Write(b[(s + 1):i]); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -66,7 +85,7 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
|||||||
state = expectString
|
state = expectString
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectString && (b[i] == '"' && (slash%2 == 0)):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '[':
|
case state == expectValue && b[i] == '[':
|
||||||
@ -104,8 +123,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +179,8 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
|||||||
e = 0
|
e = 0
|
||||||
d = 0
|
d = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slash = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if ws == -1 || (ws == 0 && we == len(b)) {
|
if ws == -1 || (ws == 0 && we == len(b)) {
|
||||||
|
32
jsn/strip.go
32
jsn/strip.go
@ -11,14 +11,27 @@ func Strip(b []byte, path [][]byte) []byte {
|
|||||||
pi := 0
|
pi := 0
|
||||||
pm := false
|
pm := false
|
||||||
state := expectKey
|
state := expectKey
|
||||||
|
instr := false
|
||||||
|
slash := 0
|
||||||
|
|
||||||
for i := 0; i < len(b); i++ {
|
for i := 0; i < len(b); i++ {
|
||||||
|
if instr && b[i] == '\\' {
|
||||||
|
slash++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b[i] == '"' && (slash%2 == 0) {
|
||||||
|
instr = !instr
|
||||||
|
}
|
||||||
|
|
||||||
if state == expectObjClose || state == expectListClose {
|
if state == expectObjClose || state == expectListClose {
|
||||||
switch b[i] {
|
if !instr {
|
||||||
case '{', '[':
|
switch b[i] {
|
||||||
d++
|
case '{', '[':
|
||||||
case '}', ']':
|
d++
|
||||||
d--
|
case '}', ']':
|
||||||
|
d--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +40,7 @@ func Strip(b []byte, path [][]byte) []byte {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectKeyClose && (b[i] == '"' && (slash%2 == 0)):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
if pi == len(path) {
|
if pi == len(path) {
|
||||||
pi = 0
|
pi = 0
|
||||||
@ -44,7 +57,7 @@ func Strip(b []byte, path [][]byte) []byte {
|
|||||||
state = expectString
|
state = expectString
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
case state == expectString && (b[i] == '"' && (slash%2 == 0)):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '[':
|
case state == expectValue && b[i] == '[':
|
||||||
@ -82,8 +95,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +114,8 @@ func Strip(b []byte, path [][]byte) []byte {
|
|||||||
state = expectKey
|
state = expectKey
|
||||||
e = 0
|
e = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slash = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return ob
|
return ob
|
||||||
|
1
jsn/test7.json
Normal file
1
jsn/test7.json
Normal file
File diff suppressed because one or more lines are too long
7
jsn/test8.json
Normal file
7
jsn/test8.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"slug": "javapackage-mainimport-nethttp-strings-githubcomgi-2786",
|
||||||
|
"published": true,
|
||||||
|
"body": "# \n\n```java\npackage main\n\nimport (\n \"net/http\"\n \"strings\"\n\n \"github.com/gin-gonic/gin\"\n)\n\nfunc main() {\n r := gin.Default()\n r.LoadHTMLGlob(\"templates/*\")\n\n r.GET(\"/\", handleIndex)\n r.GET(\"/to/:name\", handleIndex)\n r.Run()\n}\n\n// Hello is page data for the template\ntype Hello struct {\n Name string\n}\n\nfunc handleIndex(c *gin.Context) {\n name := c.Param(\"name\")\n if name != \"\" {\n name = strings.TrimPrefix(c.Param(\"name\"), \"/\")\n }\n c.HTML(http.StatusOK, \"hellofly.tmpl\", gin.H{\"Name\": name})\n}\n```\n\n\\"
|
||||||
|
}
|
||||||
|
}
|
@ -257,7 +257,7 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
// Lock to ensure multiple migrations cannot occur simultaneously
|
// Lock to ensure multiple migrations cannot occur simultaneously
|
||||||
lockNum := int64(9628173550095224) // arbitrary random number
|
lockNum := int64(9628173550095224) // arbitrary random number
|
||||||
if _, lockErr := m.conn.Exec(ctx, "select pg_advisory_lock($1)", lockNum); lockErr != nil {
|
if _, lockErr := m.conn.Exec(ctx, "select pg_try_advisory_lock($1)", lockNum); lockErr != nil {
|
||||||
return lockErr
|
return lockErr
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -331,9 +331,9 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset all database connection settings. Important to do before updating version as search_path may have been changed.
|
// Reset all database connection settings. Important to do before updating version as search_path may have been changed.
|
||||||
if _, err := tx.Exec(ctx, "reset all"); err != nil {
|
// if _, err := tx.Exec(ctx, "reset all"); err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Add one to the version
|
// Add one to the version
|
||||||
_, err = tx.Exec(ctx, "update "+m.versionTable+" set version=$1", sequence)
|
_, err = tx.Exec(ctx, "update "+m.versionTable+" set version=$1", sequence)
|
||||||
|
216
psql/columns.go
Normal file
216
psql/columns.go
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
//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)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
switch {
|
||||||
|
case isSearch && cn == "search_rank":
|
||||||
|
if err := c.renderColumnSearchRank(sel, ti, col, i); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case isSearch && strings.HasPrefix(cn, "search_headline_"):
|
||||||
|
if err := c.renderColumnSearchHeadline(sel, ti, col, i); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case cn == "__typename":
|
||||||
|
if err := c.renderColumnTypename(sel, ti, col, i); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.HasSuffix(cn, "_cursor"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
default:
|
||||||
|
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) renderColumnTypename(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
|
||||||
|
if isColumnBlocked(sel, col.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.renderComma(columnsRendered)
|
||||||
|
io.WriteString(c.w, `(`)
|
||||||
|
squoted(c.w, ti.Name)
|
||||||
|
io.WriteString(c.w, ` :: text)`)
|
||||||
|
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 := col.Name[: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,68 +12,41 @@ func simpleInsert(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
|
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
compileGQLToPSQL(t, gql, vars, "user")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func singleInsert(t *testing.T) {
|
func singleInsert(t *testing.T) {
|
||||||
gql := `mutation {
|
gql := `mutation {
|
||||||
product(id: 15, insert: $insert) {
|
product(id: $id, insert: $insert) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT "t"."name", "t"."description", "t"."price", "t"."user_id" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"insert": json.RawMessage(` { "name": "my_name", "price": 6.95, "description": "my_desc", "user_id": 5 }`),
|
"insert": json.RawMessage(` { "name": "my_name", "price": 6.95, "description": "my_desc", "user_id": 5 }`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
|
compileGQLToPSQL(t, gql, vars, "anon")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func bulkInsert(t *testing.T) {
|
func bulkInsert(t *testing.T) {
|
||||||
gql := `mutation {
|
gql := `mutation {
|
||||||
product(name: "test", id: 15, insert: $insert) {
|
product(name: "test", id: $id, insert: $insert) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"insert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`),
|
"insert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
|
compileGQLToPSQL(t, gql, vars, "anon")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func simpleInsertWithPresets(t *testing.T) {
|
func simpleInsertWithPresets(t *testing.T) {
|
||||||
@ -83,20 +56,11 @@ func simpleInsertWithPresets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`),
|
"data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
compileGQLToPSQL(t, gql, vars, "user")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedInsertManyToMany(t *testing.T) {
|
func nestedInsertManyToMany(t *testing.T) {
|
||||||
@ -118,10 +82,6 @@ func nestedInsertManyToMany(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "product_id", "customer_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "products"."id", "customers"."id" FROM "_sg_input" i, "products", "customers", json_populate_record(NULL::purchases, i.j) t RETURNING *) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products", json_populate_record(NULL::purchases, i.j) t RETURNING *) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(` {
|
"data": json.RawMessage(` {
|
||||||
"sale_type": "bought",
|
"sale_type": "bought",
|
||||||
@ -139,16 +99,7 @@ func nestedInsertManyToMany(t *testing.T) {
|
|||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql1 && string(resSQL) != sql2 {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedInsertOneToMany(t *testing.T) {
|
func nestedInsertOneToMany(t *testing.T) {
|
||||||
@ -165,8 +116,6 @@ func nestedInsertOneToMany(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j->'product') t RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
"email": "thedude@rug.com",
|
"email": "thedude@rug.com",
|
||||||
@ -182,14 +131,7 @@ func nestedInsertOneToMany(t *testing.T) {
|
|||||||
}`),
|
}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedInsertOneToOne(t *testing.T) {
|
func nestedInsertOneToOne(t *testing.T) {
|
||||||
@ -205,8 +147,6 @@ func nestedInsertOneToOne(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
"name": "Apple",
|
"name": "Apple",
|
||||||
@ -225,14 +165,7 @@ func nestedInsertOneToOne(t *testing.T) {
|
|||||||
}`),
|
}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedInsertOneToManyWithConnect(t *testing.T) {
|
func nestedInsertOneToManyWithConnect(t *testing.T) {
|
||||||
@ -249,8 +182,6 @@ func nestedInsertOneToManyWithConnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables
|
|||||||
root.Where = nil
|
root.Where = nil
|
||||||
root.Args = nil
|
root.Args = nil
|
||||||
|
|
||||||
return c.compileQuery(qc, w)
|
return c.compileQuery(qc, w, vars)
|
||||||
}
|
}
|
||||||
|
|
||||||
type kvitem struct {
|
type kvitem struct {
|
||||||
@ -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) {
|
||||||
qc, err := qcompile.Compile([]byte(gql), role)
|
generateTestFile := false
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
if generateTestFile {
|
||||||
|
var sqlStmts []string
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
qc, err := qcompile.Compile([]byte(gql), role)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
_, sqlStmt, err := pcompile.CompileEx(qc, vars)
|
for i := 0; i < 200; i++ {
|
||||||
if err != nil {
|
qc, err := qcompile.Compile([]byte(gql), role)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, sqlStmt, err := pcompile.CompileEx(qc, vars)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
failed := true
|
||||||
|
|
||||||
|
for _, sql := range expected[t.Name()] {
|
||||||
|
if string(sqlStmt) == sql {
|
||||||
|
failed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed {
|
||||||
|
fmt.Println(string(sqlStmt))
|
||||||
|
t.Fatal(errNotExpected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Println(string(sqlStmt))
|
|
||||||
|
|
||||||
return sqlStmt, nil
|
|
||||||
}
|
}
|
||||||
|
825
psql/query.go
825
psql/query.go
@ -17,6 +17,10 @@ const (
|
|||||||
closeBlock = 500
|
closeBlock = 500
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAllTablesSkipped = errors.New("all tables skipped. cannot render query")
|
||||||
|
)
|
||||||
|
|
||||||
type Variables map[string]json.RawMessage
|
type Variables map[string]json.RawMessage
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -30,7 +34,10 @@ type Compiler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewCompiler(conf Config) *Compiler {
|
func NewCompiler(conf Config) *Compiler {
|
||||||
return &Compiler{conf.Schema, conf.Vars}
|
return &Compiler{
|
||||||
|
schema: conf.Schema,
|
||||||
|
vars: conf.Vars,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error {
|
func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error {
|
||||||
@ -65,7 +72,7 @@ func (co *Compiler) CompileEx(qc *qcode.QCode, vars Variables) (uint32, []byte,
|
|||||||
func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
|
func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
|
||||||
switch qc.Type {
|
switch qc.Type {
|
||||||
case qcode.QTQuery:
|
case qcode.QTQuery:
|
||||||
return co.compileQuery(qc, w)
|
return co.compileQuery(qc, w, vars)
|
||||||
case qcode.QTInsert, qcode.QTUpdate, qcode.QTDelete, qcode.QTUpsert:
|
case qcode.QTInsert, qcode.QTUpdate, qcode.QTDelete, qcode.QTUpsert:
|
||||||
return co.compileMutation(qc, w, vars)
|
return co.compileMutation(qc, w, vars)
|
||||||
}
|
}
|
||||||
@ -73,68 +80,38 @@ func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint3
|
|||||||
return 0, fmt.Errorf("Unknown operation type %d", qc.Type)
|
return 0, fmt.Errorf("Unknown operation type %d", qc.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
|
||||||
if len(qc.Selects) == 0 {
|
if len(qc.Selects) == 0 {
|
||||||
return 0, errors.New("empty query")
|
return 0, errors.New("empty query")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &compilerContext{w, qc.Selects, co}
|
c := &compilerContext{w, qc.Selects, co}
|
||||||
multiRoot := (len(qc.Roots) > 1)
|
|
||||||
|
|
||||||
st := NewIntStack()
|
st := NewIntStack()
|
||||||
si := 0
|
i := 0
|
||||||
|
|
||||||
if multiRoot {
|
io.WriteString(c.w, `SELECT json_build_object(`)
|
||||||
io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `)
|
for _, id := range qc.Roots {
|
||||||
|
root := &qc.Selects[id]
|
||||||
for _, id := range qc.Roots {
|
if root.SkipRender || len(root.Cols) == 0 {
|
||||||
root := qc.Selects[id]
|
continue
|
||||||
if root.SkipRender {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
st.Push(root.ID + closeBlock)
|
|
||||||
st.Push(root.ID)
|
|
||||||
|
|
||||||
if si != 0 {
|
|
||||||
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 {
|
st.Push(root.ID + closeBlock)
|
||||||
io.WriteString(c.w, ` FROM `)
|
st.Push(root.ID)
|
||||||
|
|
||||||
|
if i != 0 {
|
||||||
|
io.WriteString(c.w, `, `)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
c.renderRootSelect(root)
|
||||||
root := qc.Selects[0]
|
i++
|
||||||
if !root.SkipRender {
|
|
||||||
io.WriteString(c.w, `SELECT json_object_agg(`)
|
|
||||||
io.WriteString(c.w, `'`)
|
|
||||||
io.WriteString(c.w, root.FieldName)
|
|
||||||
io.WriteString(c.w, `', `)
|
|
||||||
io.WriteString(c.w, `json_`)
|
|
||||||
int2string(c.w, root.ID)
|
|
||||||
|
|
||||||
st.Push(root.ID + closeBlock)
|
|
||||||
st.Push(root.ID)
|
|
||||||
|
|
||||||
io.WriteString(c.w, `) FROM `)
|
|
||||||
si++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if si == 0 {
|
io.WriteString(c.w, `) as "__root" FROM `)
|
||||||
return 0, errors.New("all tables skipped. cannot render query")
|
|
||||||
|
if i == 0 {
|
||||||
|
return 0, ErrAllTablesSkipped
|
||||||
}
|
}
|
||||||
|
|
||||||
var ignored uint32
|
var ignored uint32
|
||||||
@ -149,8 +126,8 @@ 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 {
|
if len(sel.Cols) == 0 {
|
||||||
io.WriteString(c.w, `(`)
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ti, err := c.schema.GetTable(sel.Name)
|
ti, err := c.schema.GetTable(sel.Name)
|
||||||
@ -158,13 +135,17 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if sel.ParentID != -1 {
|
if sel.ParentID == -1 {
|
||||||
if err = c.renderLateralJoin(sel); err != nil {
|
io.WriteString(c.w, `(`)
|
||||||
return 0, err
|
} else {
|
||||||
}
|
c.renderLateralJoin(sel)
|
||||||
}
|
}
|
||||||
|
|
||||||
skipped, err := c.renderSelect(sel, ti)
|
if !ti.Singular {
|
||||||
|
c.renderPluralSelect(sel, ti)
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped, err := c.renderSelect(sel, ti, vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@ -191,40 +172,97 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.renderSelectClose(sel, ti)
|
io.WriteString(c.w, `)`)
|
||||||
if err != nil {
|
aliasWithID(c.w, "__sr", sel.ID)
|
||||||
return 0, err
|
|
||||||
|
io.WriteString(c.w, `)`)
|
||||||
|
aliasWithID(c.w, "__sj", sel.ID)
|
||||||
|
|
||||||
|
if !ti.Singular {
|
||||||
|
io.WriteString(c.w, `)`)
|
||||||
|
aliasWithID(c.w, "__sj", sel.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sel.ParentID != -1 {
|
if sel.ParentID == -1 {
|
||||||
if err = c.renderLateralJoinClose(sel); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
io.WriteString(c.w, `)`)
|
|
||||||
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) renderPluralSelect(sel *qcode.Select, ti *DBTableInfo) error {
|
||||||
|
io.WriteString(c.w, `SELECT coalesce(json_agg("__sj_`)
|
||||||
|
int2string(c.w, sel.ID)
|
||||||
|
io.WriteString(c.w, `"."json"), '[]') as "json"`)
|
||||||
|
|
||||||
|
if sel.Paging.Type != qcode.PtOffset {
|
||||||
|
n := 0
|
||||||
|
|
||||||
|
// check if primary key already included in order by
|
||||||
|
// query argument
|
||||||
|
for _, ob := range sel.OrderBy {
|
||||||
|
if ob.Col == ti.PrimaryCol.Key {
|
||||||
|
n = 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == 1 {
|
||||||
|
n = len(sel.OrderBy)
|
||||||
|
} else {
|
||||||
|
n = len(sel.OrderBy) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(c.w, `, CONCAT_WS(','`)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
io.WriteString(c.w, `, max("__cur_`)
|
||||||
|
int2string(c.w, int32(i))
|
||||||
|
io.WriteString(c.w, `")`)
|
||||||
|
}
|
||||||
|
io.WriteString(c.w, `) as "cursor"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(c.w, ` FROM (`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compilerContext) renderRootSelect(sel *qcode.Select) error {
|
||||||
|
io.WriteString(c.w, `'`)
|
||||||
|
io.WriteString(c.w, sel.FieldName)
|
||||||
|
io.WriteString(c.w, `', `)
|
||||||
|
|
||||||
|
io.WriteString(c.w, `"__sj_`)
|
||||||
|
int2string(c.w, sel.ID)
|
||||||
|
io.WriteString(c.w, `"."json"`)
|
||||||
|
|
||||||
|
if sel.Paging.Type != qcode.PtOffset {
|
||||||
|
io.WriteString(c.w, `, '`)
|
||||||
|
io.WriteString(c.w, sel.FieldName)
|
||||||
|
io.WriteString(c.w, `_cursor', `)
|
||||||
|
|
||||||
|
io.WriteString(c.w, `"__sj_`)
|
||||||
|
int2string(c.w, sel.ID)
|
||||||
|
io.WriteString(c.w, `"."cursor"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compilerContext) initSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) (uint32, []*qcode.Column, error) {
|
||||||
var skipped uint32
|
var skipped uint32
|
||||||
|
|
||||||
cols := make([]*qcode.Column, 0, len(sel.Cols))
|
cols := make([]*qcode.Column, 0, len(sel.Cols))
|
||||||
@ -238,6 +276,31 @@ 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{}{}
|
||||||
|
addPrimaryKey := true
|
||||||
|
|
||||||
|
for _, ob := range sel.OrderBy {
|
||||||
|
if ob.Col == ti.PrimaryCol.Key {
|
||||||
|
addPrimaryKey = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if addPrimaryKey {
|
||||||
|
ob := &qcode.OrderBy{Col: ti.PrimaryCol.Name, Order: qcode.OrderAsc}
|
||||||
|
|
||||||
|
if sel.Paging.Type == qcode.PtBackward {
|
||||||
|
ob.Order = qcode.OrderDesc
|
||||||
|
}
|
||||||
|
sel.OrderBy = append(sel.OrderBy, ob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sel.Paging.Cursor {
|
||||||
|
c.addSeekPredicate(sel)
|
||||||
|
}
|
||||||
|
|
||||||
for _, id := range sel.Children {
|
for _, id := range sel.Children {
|
||||||
child := &c.s[id]
|
child := &c.s[id]
|
||||||
|
|
||||||
@ -283,7 +346,73 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
|
|||||||
return skipped, cols, nil
|
return skipped, cols, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint32, error) {
|
// This
|
||||||
|
// (A, B, C) >= (X, Y, Z)
|
||||||
|
//
|
||||||
|
// Becomes
|
||||||
|
// (A > X)
|
||||||
|
// OR ((A = X) AND (B > Y))
|
||||||
|
// OR ((A = X) AND (B = Y) AND (C > Z))
|
||||||
|
// OR ((A = X) AND (B = Y) AND (C = Z))
|
||||||
|
|
||||||
|
func (c *compilerContext) addSeekPredicate(sel *qcode.Select) error {
|
||||||
|
var or, and *qcode.Exp
|
||||||
|
|
||||||
|
obLen := len(sel.OrderBy)
|
||||||
|
|
||||||
|
if obLen > 1 {
|
||||||
|
or = qcode.NewFilter()
|
||||||
|
or.Op = qcode.OpOr
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < obLen; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
and = qcode.NewFilter()
|
||||||
|
and.Op = qcode.OpAnd
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, ob := range sel.OrderBy {
|
||||||
|
f := qcode.NewFilter()
|
||||||
|
f.Col = ob.Col
|
||||||
|
f.Type = qcode.ValRef
|
||||||
|
f.Table = "__cur"
|
||||||
|
f.Val = ob.Col
|
||||||
|
|
||||||
|
if obLen == 1 {
|
||||||
|
qcode.AddFilter(sel, f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case i > 0 && n != i:
|
||||||
|
f.Op = qcode.OpEquals
|
||||||
|
case ob.Order == qcode.OrderDesc:
|
||||||
|
f.Op = qcode.OpLesserThan
|
||||||
|
default:
|
||||||
|
f.Op = qcode.OpGreaterThan
|
||||||
|
}
|
||||||
|
|
||||||
|
if and != nil {
|
||||||
|
and.Children = append(and.Children, f)
|
||||||
|
} else {
|
||||||
|
or.Children = append(or.Children, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == i {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if and != nil {
|
||||||
|
or.Children = append(or.Children, and)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qcode.AddFilter(sel, or)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) (uint32, error) {
|
||||||
var rel *DBRel
|
var rel *DBRel
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -296,131 +425,70 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
skipped, childCols, err := c.processChildren(sel, ti)
|
skipped, childCols, err := c.initSelect(sel, ti, vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
hasOrder := len(sel.OrderBy) != 0
|
|
||||||
|
|
||||||
// SELECT
|
// SELECT
|
||||||
if !ti.Singular {
|
// io.WriteString(c.w, `SELECT json_build_object(`)
|
||||||
//fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Name)
|
// if err := c.renderColumns(sel, ti, skipped); err != nil {
|
||||||
io.WriteString(c.w, `SELECT coalesce(json_agg("`)
|
// return 0, err
|
||||||
io.WriteString(c.w, "json_")
|
// }
|
||||||
int2string(c.w, sel.ID)
|
|
||||||
io.WriteString(c.w, `"`)
|
|
||||||
|
|
||||||
if hasOrder {
|
io.WriteString(c.w, `SELECT row_to_json("__sr_`)
|
||||||
if err := c.renderOrderBy(sel, ti); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Name)
|
|
||||||
io.WriteString(c.w, `), '[]')`)
|
|
||||||
aliasWithID(c.w, "json", sel.ID)
|
|
||||||
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)
|
int2string(c.w, sel.ID)
|
||||||
io.WriteString(c.w, `" FROM (SELECT `)
|
io.WriteString(c.w, `") AS "json"`)
|
||||||
|
|
||||||
// Combined column names
|
if sel.Paging.Type != qcode.PtOffset {
|
||||||
c.renderColumns(sel, ti)
|
for i := range sel.OrderBy {
|
||||||
|
io.WriteString(c.w, `, "__cur_`)
|
||||||
c.renderRemoteRelColumns(sel, ti)
|
int2string(c.w, int32(i))
|
||||||
|
io.WriteString(c.w, `"`)
|
||||||
if err = c.renderJoinedColumns(sel, ti, skipped); err != nil {
|
}
|
||||||
return skipped, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Fprintf(w, `) AS "%d"`, c.sel.ID)
|
io.WriteString(c.w, `FROM (SELECT `)
|
||||||
io.WriteString(c.w, `)`)
|
|
||||||
aliasWithID(c.w, "json_row", sel.ID)
|
|
||||||
|
|
||||||
//fmt.Fprintf(w, `)) AS "%s"`, c.sel.Name)
|
if err := c.renderColumns(sel, ti, skipped); err != nil {
|
||||||
io.WriteString(c.w, `))`)
|
return 0, err
|
||||||
aliasWithID(c.w, "json", sel.ID)
|
|
||||||
// END-ROW-TO-JSON
|
|
||||||
|
|
||||||
if hasOrder {
|
|
||||||
c.renderOrderByColumns(sel, ti)
|
|
||||||
}
|
}
|
||||||
// END-SELECT
|
|
||||||
|
if sel.Paging.Type != qcode.PtOffset {
|
||||||
|
for i, ob := range sel.OrderBy {
|
||||||
|
io.WriteString(c.w, `, LAST_VALUE(`)
|
||||||
|
colWithTableID(c.w, ti.Name, sel.ID, ob.Col)
|
||||||
|
io.WriteString(c.w, `) OVER() AS "__cur_`)
|
||||||
|
int2string(c.w, int32(i))
|
||||||
|
io.WriteString(c.w, `"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(c.w, ` FROM (`)
|
||||||
|
|
||||||
// FROM (SELECT .... )
|
// 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, `)`)
|
// aliasWithID(c.w, "__sj", sel.ID)
|
||||||
aliasWithIDSuffix(c.w, sel.Name, sel.ID, "_join")
|
|
||||||
io.WriteString(c.w, ` ON ('true')`)
|
io.WriteString(c.w, ` ON ('true')`)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -460,39 +528,47 @@ 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
|
||||||
|
var cn string
|
||||||
|
|
||||||
for _, col := range sel.Cols {
|
for _, col := range sel.Cols {
|
||||||
n := funcPrefixLen(col.Name)
|
if n := funcPrefixLen(col.Name); n != 0 {
|
||||||
if n != 0 {
|
|
||||||
if !sel.Functions {
|
if !sel.Functions {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(sel.Allowed) != 0 {
|
cn = col.Name[n:]
|
||||||
if _, ok := sel.Allowed[col.Name[n:]]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if len(sel.Allowed) != 0 {
|
cn = col.Name
|
||||||
if _, ok := sel.Allowed[col.Name]; !ok {
|
|
||||||
continue
|
if strings.HasSuffix(cn, "_cursor") {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sel.Allowed) != 0 {
|
||||||
|
if _, ok := sel.Allowed[cn]; !ok {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
colWithTableID(c.w, ti.Name, sel.ID, col.Name)
|
||||||
colWithTableIDAlias(c.w, ti.Name, sel.ID, col.Name, col.FieldName)
|
alias(c.w, col.FieldName)
|
||||||
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i += c.renderRemoteRelColumns(sel, ti, i)
|
||||||
|
|
||||||
|
return c.renderJoinColumns(sel, ti, skipped, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo) {
|
func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo, colsRendered int) int {
|
||||||
i := 0
|
i := colsRendered
|
||||||
|
|
||||||
for _, id := range sel.Children {
|
for _, id := range sel.Children {
|
||||||
child := &c.s[id]
|
child := &c.s[id]
|
||||||
@ -504,44 +580,48 @@ 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)
|
|
||||||
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)
|
alias(c.w, rel.Right.Col)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error {
|
func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32, colsRendered int) error {
|
||||||
|
|
||||||
// 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)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
childSel := &c.s[id]
|
childSel := &c.s[id]
|
||||||
if childSel.SkipRender {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
io.WriteString(c.w, ", ")
|
io.WriteString(c.w, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
|
if childSel.SkipRender {
|
||||||
//s.Name, s.ID, s.Name, s.FieldName)
|
io.WriteString(c.w, `NULL`)
|
||||||
//if cti.Singular {
|
alias(c.w, childSel.FieldName)
|
||||||
io.WriteString(c.w, `"`)
|
continue
|
||||||
io.WriteString(c.w, childSel.Name)
|
}
|
||||||
io.WriteString(c.w, `_`)
|
|
||||||
|
io.WriteString(c.w, `"__sj_`)
|
||||||
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)
|
alias(c.w, childSel.FieldName)
|
||||||
io.WriteString(c.w, `" AS "`)
|
|
||||||
io.WriteString(c.w, childSel.FieldName)
|
if childSel.Paging.Type != qcode.PtOffset {
|
||||||
io.WriteString(c.w, `"`)
|
io.WriteString(c.w, `, "__sj_`)
|
||||||
|
int2string(c.w, childSel.ID)
|
||||||
|
io.WriteString(c.w, `"."cursor" AS "`)
|
||||||
|
io.WriteString(c.w, childSel.FieldName)
|
||||||
|
io.WriteString(c.w, `_cursor"`)
|
||||||
|
}
|
||||||
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -550,171 +630,29 @@ 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)))
|
if sel.Paging.Cursor {
|
||||||
|
c.renderCursorCTE(sel)
|
||||||
io.WriteString(c.w, ` FROM (SELECT `)
|
|
||||||
|
|
||||||
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 {
|
io.WriteString(c.w, `SELECT `)
|
||||||
if _, ok := colmap[ob.Col]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
colmap[ob.Col] = struct{}{}
|
|
||||||
|
|
||||||
if i != 0 {
|
if len(sel.DistinctOn) != 0 {
|
||||||
io.WriteString(c.w, `, `)
|
c.renderDistinctOn(sel, ti)
|
||||||
}
|
|
||||||
colWithTable(c.w, ti.Name, ob.Col)
|
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, col := range childCols {
|
realColsRendered, isAgg, err := c.renderBaseColumns(sel, ti, childCols, skipped)
|
||||||
if _, ok := colmap[col.Name]; ok {
|
if err != nil {
|
||||||
continue
|
return err
|
||||||
}
|
|
||||||
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,17 +679,19 @@ 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)
|
||||||
}
|
colWithTable(c.w, ti.Name, 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)
|
}
|
||||||
}
|
|
||||||
|
if hasOrder {
|
||||||
|
if err := c.renderOrderBy(sel, ti); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -779,10 +719,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -821,24 +757,26 @@ func (c *compilerContext) renderFrom(sel *qcode.Select, ti *DBTableInfo, rel *DB
|
|||||||
io.WriteString(c.w, `"`)
|
io.WriteString(c.w, `"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sel.Paging.Cursor {
|
||||||
|
io.WriteString(c.w, `, "__cur"`)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInfo) {
|
func (c *compilerContext) renderCursorCTE(sel *qcode.Select) error {
|
||||||
//colsRendered := len(sel.Cols) != 0
|
io.WriteString(c.w, `WITH "__cur" AS (SELECT `)
|
||||||
|
for i, ob := range sel.OrderBy {
|
||||||
for i := range sel.OrderBy {
|
if i != 0 {
|
||||||
//io.WriteString(w, ", ")
|
io.WriteString(c.w, `, `)
|
||||||
io.WriteString(c.w, `, `)
|
}
|
||||||
|
io.WriteString(c.w, `a[`)
|
||||||
col := sel.OrderBy[i].Col
|
int2string(c.w, int32(i+1))
|
||||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`,
|
io.WriteString(c.w, `] as `)
|
||||||
//c.sel.Name, c.sel.ID, c,
|
quoted(c.w, ob.Col)
|
||||||
//c.sel.Name, c.sel.ID, c)
|
|
||||||
colWithTableID(c.w, ti.Name, sel.ID, col)
|
|
||||||
io.WriteString(c.w, ` AS `)
|
|
||||||
tableIDColSuffix(c.w, sel.Name, sel.ID, col, "_ob")
|
|
||||||
}
|
}
|
||||||
|
io.WriteString(c.w, ` FROM string_to_array('{{cursor}}', ',') as a) `)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
||||||
@ -961,7 +899,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 +909,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 +929,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
|
||||||
@ -1066,8 +1002,12 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error {
|
|||||||
|
|
||||||
switch ex.Op {
|
switch ex.Op {
|
||||||
case qcode.OpEquals:
|
case qcode.OpEquals:
|
||||||
io.WriteString(c.w, `IS NOT DISTINCT FROM`)
|
io.WriteString(c.w, `=`)
|
||||||
case qcode.OpNotEquals:
|
case qcode.OpNotEquals:
|
||||||
|
io.WriteString(c.w, `!=`)
|
||||||
|
case qcode.OpNotDistinct:
|
||||||
|
io.WriteString(c.w, `IS NOT DISTINCT FROM`)
|
||||||
|
case qcode.OpDistinct:
|
||||||
io.WriteString(c.w, `IS DISTINCT FROM`)
|
io.WriteString(c.w, `IS DISTINCT FROM`)
|
||||||
case qcode.OpGreaterOrEquals:
|
case qcode.OpGreaterOrEquals:
|
||||||
io.WriteString(c.w, `>=`)
|
io.WriteString(c.w, `>=`)
|
||||||
@ -1130,23 +1070,24 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error {
|
|||||||
io.WriteString(c.w, `((`)
|
io.WriteString(c.w, `((`)
|
||||||
colWithTable(c.w, ti.Name, ti.TSVCol.Name)
|
colWithTable(c.w, ti.Name, ti.TSVCol.Name)
|
||||||
if c.schema.ver >= 110000 {
|
if c.schema.ver >= 110000 {
|
||||||
io.WriteString(c.w, `) @@ websearch_to_tsquery('`)
|
io.WriteString(c.w, `) @@ websearch_to_tsquery('{{`)
|
||||||
} else {
|
} else {
|
||||||
io.WriteString(c.w, `) @@ to_tsquery('`)
|
io.WriteString(c.w, `) @@ to_tsquery('{{`)
|
||||||
}
|
}
|
||||||
io.WriteString(c.w, ex.Val)
|
io.WriteString(c.w, ex.Val)
|
||||||
io.WriteString(c.w, `'))`)
|
io.WriteString(c.w, `}}'))`)
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("[Where] unexpected op code %d", ex.Op)
|
return fmt.Errorf("[Where] unexpected op code %d", ex.Op)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ex.Type == qcode.ValList {
|
switch {
|
||||||
|
case ex.Type == qcode.ValList:
|
||||||
c.renderList(ex)
|
c.renderList(ex)
|
||||||
} else if col == nil {
|
case col == nil:
|
||||||
return errors.New("no column found for expression value")
|
return errors.New("no column found for expression value")
|
||||||
} else {
|
default:
|
||||||
c.renderVal(ex, c.vars, col)
|
c.renderVal(ex, c.vars, col)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1161,31 +1102,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 +1130,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, `) `)
|
||||||
}
|
}
|
||||||
@ -1228,32 +1157,30 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *
|
|||||||
io.WriteString(c.w, ` `)
|
io.WriteString(c.w, ` `)
|
||||||
|
|
||||||
switch ex.Type {
|
switch ex.Type {
|
||||||
case qcode.ValBool, qcode.ValInt, qcode.ValFloat:
|
|
||||||
if len(ex.Val) != 0 {
|
|
||||||
io.WriteString(c.w, ex.Val)
|
|
||||||
} else {
|
|
||||||
io.WriteString(c.w, `''`)
|
|
||||||
}
|
|
||||||
|
|
||||||
case qcode.ValStr:
|
|
||||||
io.WriteString(c.w, `'`)
|
|
||||||
io.WriteString(c.w, ex.Val)
|
|
||||||
io.WriteString(c.w, `'`)
|
|
||||||
|
|
||||||
case qcode.ValVar:
|
case qcode.ValVar:
|
||||||
io.WriteString(c.w, `'`)
|
val, ok := vars[ex.Val]
|
||||||
if val, ok := vars[ex.Val]; ok {
|
switch {
|
||||||
io.WriteString(c.w, val)
|
case ok && strings.HasPrefix(val, "sql:"):
|
||||||
} else {
|
io.WriteString(c.w, ` (`)
|
||||||
//fmt.Fprintf(w, `'{{%s}}'`, ex.Val)
|
io.WriteString(c.w, val[4:])
|
||||||
io.WriteString(c.w, `{{`)
|
io.WriteString(c.w, `)`)
|
||||||
|
case ok:
|
||||||
|
squoted(c.w, val)
|
||||||
|
default:
|
||||||
|
io.WriteString(c.w, ` '{{`)
|
||||||
io.WriteString(c.w, ex.Val)
|
io.WriteString(c.w, ex.Val)
|
||||||
io.WriteString(c.w, `}}`)
|
io.WriteString(c.w, `}}'`)
|
||||||
}
|
}
|
||||||
io.WriteString(c.w, `' :: `)
|
|
||||||
io.WriteString(c.w, col.Type)
|
case qcode.ValRef:
|
||||||
|
colWithTable(c.w, ex.Table, ex.Col)
|
||||||
|
|
||||||
|
default:
|
||||||
|
squoted(c.w, ex.Val)
|
||||||
}
|
}
|
||||||
//io.WriteString(c.w, `)`)
|
|
||||||
|
io.WriteString(c.w, ` :: `)
|
||||||
|
io.WriteString(c.w, col.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
func funcPrefixLen(fn string) int {
|
func funcPrefixLen(fn string) int {
|
||||||
@ -1303,15 +1230,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 +1250,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"
|
||||||
|
@ -2,6 +2,7 @@ package psql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -10,16 +11,16 @@ func withComplexArgs(t *testing.T) {
|
|||||||
proDUcts(
|
proDUcts(
|
||||||
# returns only 30 items
|
# returns only 30 items
|
||||||
limit: 30,
|
limit: 30,
|
||||||
|
|
||||||
# starts from item 10, commented out for now
|
# starts from item 10, commented out for now
|
||||||
# offset: 10,
|
# offset: 10,
|
||||||
|
|
||||||
# orders the response items by highest price
|
# orders the response items by highest price
|
||||||
order_by: { price: desc },
|
order_by: { price: desc },
|
||||||
|
|
||||||
# no duplicate prices returned
|
# no duplicate prices returned
|
||||||
distinct: [ price ]
|
distinct: [ price ]
|
||||||
|
|
||||||
# only items with an id >= 20 and < 28 are returned
|
# only items with an id >= 20 and < 28 are returned
|
||||||
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) {
|
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) {
|
||||||
id
|
id
|
||||||
@ -28,16 +29,41 @@ func withComplexArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0" ORDER BY "products_0_price_ob" DESC), '[]') AS "json_0" FROM (SELECT DISTINCT ON ("products_0_price_ob") row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0", "products_0"."price" AS "products_0_price_ob" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."id") < 28) AND (("products"."id") >= 20)))) LIMIT ('30') :: integer) AS "products_0" ORDER BY "products_0_price_ob" DESC LIMIT ('30') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
func withWhereAndList(t *testing.T) {
|
||||||
if err != nil {
|
gql := `query {
|
||||||
t.Fatal(err)
|
products(
|
||||||
}
|
where: {
|
||||||
|
and: [
|
||||||
|
{ not: { id: { is_null: true } } },
|
||||||
|
{ price: { gt: 10 } },
|
||||||
|
] } ) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
price
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
t.Fatal(errNotExpected)
|
}
|
||||||
}
|
|
||||||
|
func withWhereIsNull(t *testing.T) {
|
||||||
|
gql := `query {
|
||||||
|
products(
|
||||||
|
where: {
|
||||||
|
and: {
|
||||||
|
not: { id: { is_null: true } },
|
||||||
|
price: { gt: 10 }
|
||||||
|
}}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
price
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
}
|
}
|
||||||
|
|
||||||
func withWhereMultiOr(t *testing.T) {
|
func withWhereMultiOr(t *testing.T) {
|
||||||
@ -56,93 +82,23 @@ func withWhereMultiOr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") < 20) OR (("products"."price") > 10) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withWhereIsNull(t *testing.T) {
|
|
||||||
gql := `query {
|
|
||||||
products(
|
|
||||||
where: {
|
|
||||||
and: {
|
|
||||||
not: { id: { is_null: true } },
|
|
||||||
price: { gt: 10 }
|
|
||||||
}}) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
price
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") > 10) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withWhereAndList(t *testing.T) {
|
|
||||||
gql := `query {
|
|
||||||
products(
|
|
||||||
where: {
|
|
||||||
and: [
|
|
||||||
{ not: { id: { is_null: true } } },
|
|
||||||
{ price: { gt: 10 } },
|
|
||||||
] } ) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
price
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") > 10) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchByID(t *testing.T) {
|
func fetchByID(t *testing.T) {
|
||||||
gql := `query {
|
gql := `query {
|
||||||
product(id: 15) {
|
product(id: $id) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") = 15))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchQuery(t *testing.T) {
|
func searchQuery(t *testing.T) {
|
||||||
gql := `query {
|
gql := `query {
|
||||||
products(search: "ale") {
|
products(search: $query) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
search_rank
|
search_rank
|
||||||
@ -150,16 +106,7 @@ func searchQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."search_rank" AS "search_rank", "products_0"."search_headline_description" AS "search_headline_description") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('ale')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('ale')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('ale'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "admin")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "admin")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func oneToMany(t *testing.T) {
|
func oneToMany(t *testing.T) {
|
||||||
@ -173,16 +120,7 @@ func oneToMany(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND ((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func oneToManyReverse(t *testing.T) {
|
func oneToManyReverse(t *testing.T) {
|
||||||
@ -196,16 +134,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 +156,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 +170,7 @@ func manyToMany(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "customers_1_join"."json_1" AS "customers") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "json_row_1")) AS "json_1" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func manyToManyReverse(t *testing.T) {
|
func manyToManyReverse(t *testing.T) {
|
||||||
@ -273,16 +184,7 @@ func manyToManyReverse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('customers', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func aggFunction(t *testing.T) {
|
func aggFunction(t *testing.T) {
|
||||||
@ -293,16 +195,7 @@ func aggFunction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func aggFunctionBlockedByCol(t *testing.T) {
|
func aggFunctionBlockedByCol(t *testing.T) {
|
||||||
@ -313,16 +206,7 @@ func aggFunctionBlockedByCol(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "anon")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "anon")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func aggFunctionDisabled(t *testing.T) {
|
func aggFunctionDisabled(t *testing.T) {
|
||||||
@ -333,16 +217,7 @@ func aggFunctionDisabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "anon1")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "anon1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func aggFunctionWithFilter(t *testing.T) {
|
func aggFunctionWithFilter(t *testing.T) {
|
||||||
@ -353,16 +228,7 @@ func aggFunctionWithFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."max_price" AS "max_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", max("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") > 10))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func syntheticTables(t *testing.T) {
|
func syntheticTables(t *testing.T) {
|
||||||
@ -372,16 +238,7 @@ func syntheticTables(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('me', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT ) AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryWithVariables(t *testing.T) {
|
func queryWithVariables(t *testing.T) {
|
||||||
@ -392,16 +249,7 @@ func queryWithVariables(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") IS NOT DISTINCT FROM '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint)))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func withWhereOnRelations(t *testing.T) {
|
func withWhereOnRelations(t *testing.T) {
|
||||||
@ -418,16 +266,7 @@ func withWhereOnRelations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > 3)))) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "user")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func multiRoot(t *testing.T) {
|
func multiRoot(t *testing.T) {
|
||||||
@ -451,16 +290,24 @@ 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")
|
func withCursor(t *testing.T) {
|
||||||
if err != nil {
|
gql := `query {
|
||||||
t.Fatal(err)
|
Products(
|
||||||
|
first: 20
|
||||||
|
after: $cursor
|
||||||
|
order_by: { price: desc }) {
|
||||||
|
Name
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
vars := map[string]json.RawMessage{
|
||||||
|
"cursor": json.RawMessage(`"0,1"`),
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonColumnAsTable(t *testing.T) {
|
func jsonColumnAsTable(t *testing.T) {
|
||||||
@ -477,19 +324,10 @@ 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 nullForAuthRequiredInAnon(t *testing.T) {
|
||||||
gql := `query {
|
gql := `query {
|
||||||
products {
|
products {
|
||||||
id
|
id
|
||||||
@ -501,37 +339,19 @@ 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) {
|
||||||
gql := `query {
|
gql := `query {
|
||||||
user(id: 5, where: { id: { gt: 3 } }) {
|
user(id: $id, where: { id: { gt: 3 } }) {
|
||||||
id
|
id
|
||||||
full_name
|
full_name
|
||||||
email
|
email
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "bad_dude")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func blockedFunctions(t *testing.T) {
|
func blockedFunctions(t *testing.T) {
|
||||||
@ -542,16 +362,7 @@ func blockedFunctions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE (false) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
compileGQLToPSQL(t, gql, nil, "bad_dude")
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompileQuery(t *testing.T) {
|
func TestCompileQuery(t *testing.T) {
|
||||||
@ -575,7 +386,8 @@ func TestCompileQuery(t *testing.T) {
|
|||||||
t.Run("withWhereOnRelations", withWhereOnRelations)
|
t.Run("withWhereOnRelations", withWhereOnRelations)
|
||||||
t.Run("multiRoot", multiRoot)
|
t.Run("multiRoot", multiRoot)
|
||||||
t.Run("jsonColumnAsTable", jsonColumnAsTable)
|
t.Run("jsonColumnAsTable", jsonColumnAsTable)
|
||||||
t.Run("skipUserIDForAnonRole", skipUserIDForAnonRole)
|
t.Run("withCursor", withCursor)
|
||||||
|
t.Run("nullForAuthRequiredInAnon", nullForAuthRequiredInAnon)
|
||||||
t.Run("blockedQuery", blockedQuery)
|
t.Run("blockedQuery", blockedQuery)
|
||||||
t.Run("blockedFunctions", blockedFunctions)
|
t.Run("blockedFunctions", blockedFunctions)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,14 @@ func NewDBSchema(info *DBInfo, aliases map[string][]string) (*DBSchema, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, t := range info.Tables {
|
for i, t := range info.Tables {
|
||||||
err := schema.updateRelationships(t, info.Columns[i])
|
err := schema.firstDegreeRels(t, info.Columns[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, t := range info.Tables {
|
||||||
|
err := schema.secondDegreeRels(t, info.Columns[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -131,8 +138,7 @@ func (s *DBSchema) addTable(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
|
func (s *DBSchema) firstDegreeRels(t DBTable, cols []DBColumn) error {
|
||||||
jcols := make([]DBColumn, 0, len(cols))
|
|
||||||
ct := t.Key
|
ct := t.Key
|
||||||
cti, ok := s.t[ct]
|
cti, ok := s.t[ct]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -230,6 +236,51 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
|
|||||||
if err := s.SetRel(ft, ct, rel2); err != nil {
|
if err := s.SetRel(ft, ct, rel2); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DBSchema) secondDegreeRels(t DBTable, cols []DBColumn) error {
|
||||||
|
jcols := make([]DBColumn, 0, len(cols))
|
||||||
|
ct := t.Key
|
||||||
|
cti, ok := s.t[ct]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid foreign key table '%s'", ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cols {
|
||||||
|
c := cols[i]
|
||||||
|
|
||||||
|
if len(c.FKeyTable) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foreign key column name
|
||||||
|
ft := strings.ToLower(c.FKeyTable)
|
||||||
|
|
||||||
|
ti, ok := s.t[ft]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid foreign key table '%s'", ft)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is an embedded relationship like when a json/jsonb column
|
||||||
|
// is exposed as a table
|
||||||
|
if c.Name == c.FKeyTable && len(c.FKeyColID) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.FKeyColID) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foreign key column id
|
||||||
|
fcid := c.FKeyColID[0]
|
||||||
|
|
||||||
|
if _, ok := ti.ColIDMap[fcid]; !ok {
|
||||||
|
return fmt.Errorf("invalid foreign key column id '%d' for table '%s'",
|
||||||
|
fcid, ti.Name)
|
||||||
|
}
|
||||||
|
|
||||||
jcols = append(jcols, c)
|
jcols = append(jcols, c)
|
||||||
}
|
}
|
||||||
@ -322,6 +373,9 @@ func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DBSchema) SetRel(child, parent string, rel *DBRel) error {
|
func (s *DBSchema) SetRel(child, parent string, rel *DBRel) error {
|
||||||
|
sp := strings.ToLower(flect.Singularize(parent))
|
||||||
|
pp := strings.ToLower(flect.Pluralize(parent))
|
||||||
|
|
||||||
sc := strings.ToLower(flect.Singularize(child))
|
sc := strings.ToLower(flect.Singularize(child))
|
||||||
pc := strings.ToLower(flect.Pluralize(child))
|
pc := strings.ToLower(flect.Pluralize(child))
|
||||||
|
|
||||||
@ -333,9 +387,6 @@ func (s *DBSchema) SetRel(child, parent string, rel *DBRel) error {
|
|||||||
s.rm[pc] = make(map[string]*DBRel)
|
s.rm[pc] = make(map[string]*DBRel)
|
||||||
}
|
}
|
||||||
|
|
||||||
sp := strings.ToLower(flect.Singularize(parent))
|
|
||||||
pp := strings.ToLower(flect.Pluralize(parent))
|
|
||||||
|
|
||||||
if _, ok := s.rm[sc][sp]; !ok {
|
if _, ok := s.rm[sc][sp]; !ok {
|
||||||
s.rm[sc][sp] = rel
|
s.rm[sc][sp] = rel
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,10 @@ func (rt RelType) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (re *DBRel) String() string {
|
func (re *DBRel) String() string {
|
||||||
|
if re.Type == RelOneToManyThrough {
|
||||||
|
return fmt.Sprintf("'%s.%s' --(Through: %s)--> '%s.%s'",
|
||||||
|
re.Left.Table, re.Left.Col, re.Through, re.Right.Table, re.Right.Col)
|
||||||
|
}
|
||||||
return fmt.Sprintf("'%s.%s' --(%s)--> '%s.%s'",
|
return fmt.Sprintf("'%s.%s' --(%s)--> '%s.%s'",
|
||||||
re.Left.Table, re.Left.Col, re.Type, re.Right.Table, re.Right.Col)
|
re.Left.Table, re.Left.Col, re.Type, re.Right.Table, re.Right.Col)
|
||||||
}
|
}
|
||||||
|
@ -167,7 +167,7 @@ SELECT
|
|||||||
ELSE ''::text
|
ELSE ''::text
|
||||||
END AS foreignkey,
|
END AS foreignkey,
|
||||||
CASE
|
CASE
|
||||||
WHEN p.contype = ('f'::char) THEN p.confkey
|
WHEN p.contype = ('f'::char) THEN p.confkey::int2[]
|
||||||
ELSE ARRAY[]::int2[]
|
ELSE ARRAY[]::int2[]
|
||||||
END AS foreignkey_fieldnum
|
END AS foreignkey_fieldnum
|
||||||
FROM pg_attribute f
|
FROM pg_attribute f
|
||||||
@ -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 ""
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
@ -92,7 +92,14 @@ func getTestSchema() *DBSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, t := range tables {
|
for i, t := range tables {
|
||||||
err := schema.updateRelationships(t, columns[i])
|
err := schema.firstDegreeRels(t, columns[i])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, t := range tables {
|
||||||
|
err := schema.secondDegreeRels(t, columns[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
151
psql/tests.sql
Normal file
151
psql/tests.sql
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
=== RUN TestCompileInsert
|
||||||
|
=== RUN TestCompileInsert/simpleInsert
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."id" AS "id" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/singleInsert
|
||||||
|
WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/bulkInsert
|
||||||
|
WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/simpleInsertWithPresets
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/nestedInsertManyToMany
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_2") AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_2") AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/nestedInsertOneToMany
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/nestedInsertOneToOne
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/nestedInsertOneToManyWithConnect
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/nestedInsertOneToOneWithConnect
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user", "__sj_2"."json" AS "tags" FROM (SELECT "products"."id", "products"."name", "products"."user_id", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_2") AS "json"FROM (SELECT "tags_2"."id" AS "id", "tags_2"."name" AS "name" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileInsert/nestedInsertOneToOneWithConnectArray
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id" = ANY((select a::bigint AS list from json_array_elements_text((i.j->'user'->'connect'->>'id')::json) AS a)) LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
--- PASS: TestCompileInsert (0.02s)
|
||||||
|
--- PASS: TestCompileInsert/simpleInsert (0.00s)
|
||||||
|
--- PASS: TestCompileInsert/singleInsert (0.00s)
|
||||||
|
--- 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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileMutate/singleUpsertWhere
|
||||||
|
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileMutate/bulkUpsert
|
||||||
|
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "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', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileMutate/delete
|
||||||
|
WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '1' :: bigint)) RETURNING "products".*) SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
--- PASS: TestCompileMutate (0.00s)
|
||||||
|
--- PASS: TestCompileMutate/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', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price" FROM (SELECT DISTINCT ON ("products"."price") "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."id") < '28' :: bigint) AND (("products"."id") >= '20' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) ORDER BY "products"."price" DESC LIMIT ('30') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/withWhereAndList
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/withWhereIsNull
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/withWhereMultiOr
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") < '20' :: numeric(7,2)) OR (("products"."price") > '10' :: numeric(7,2)) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/fetchByID
|
||||||
|
SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '{{id}}' :: bigint))) LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/searchQuery
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."search_rank" AS "search_rank", "products_0"."search_headline_description" AS "search_headline_description" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('{{query}}')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('{{query}}')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('{{query}}'))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/oneToMany
|
||||||
|
SELECT json_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."email" AS "email", "__sj_1"."json" AS "products" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/oneToManyReverse
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "__sj_1"."json" AS "users" 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("__sj_1"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "users_1"."email" AS "email" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/oneToManyArray
|
||||||
|
SELECT json_build_object('tags', "__sj_0"."json", 'product', "__sj_2"."json") as "__root" FROM (SELECT row_to_json("__sr_2") AS "json"FROM (SELECT "products_2"."name" AS "name", "products_2"."price" AS "price", "__sj_3"."json" AS "tags" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sj_3"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_3") AS "json"FROM (SELECT "tags_3"."id" AS "id", "tags_3"."name" AS "name" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3") AS "__sr_3") AS "__sj_3") AS "__sj_3" ON ('true')) AS "__sr_2") AS "__sj_2", (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "tags_0"."name" AS "name", "__sj_1"."json" AS "product" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."name" AS "name" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/manyToMany
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."name" AS "name", "__sj_1"."json" AS "customers" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/manyToManyReverse
|
||||||
|
SELECT json_build_object('customers', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "__sj_1"."json" AS "products" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."name" AS "name" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/aggFunction
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/aggFunctionBlockedByCol
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."name" AS "name" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/aggFunctionDisabled
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."name" AS "name" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/aggFunctionWithFilter
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."max_price" AS "max_price" FROM (SELECT "products"."id", max("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") > '10' :: bigint))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/syntheticTables
|
||||||
|
SELECT json_build_object('me', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/queryWithVariables
|
||||||
|
SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") = '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/withWhereOnRelations
|
||||||
|
SELECT json_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > '3' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/multiRoot
|
||||||
|
SELECT json_build_object('customer', "__sj_0"."json", 'user', "__sj_1"."json", 'product', "__sj_2"."json") as "__root" FROM (SELECT row_to_json("__sr_2") AS "json"FROM (SELECT "products_2"."id" AS "id", "products_2"."name" AS "name", "__sj_3"."json" AS "customers", "__sj_4"."json" AS "customer" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_4") AS "json"FROM (SELECT "customers_4"."email" AS "email" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4") AS "__sr_4") AS "__sj_4" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sj_3"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_3") AS "json"FROM (SELECT "customers_3"."email" AS "email" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3") AS "__sr_3") AS "__sj_3") AS "__sj_3" ON ('true')) AS "__sr_2") AS "__sj_2", (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1", (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "customers_0"."id" AS "id" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/jsonColumnAsTable
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "tag_count" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "tag_count_1"."count" AS "count", "__sj_2"."json" AS "tags" FROM (SELECT "tag_count"."count", "tag_count"."tag_id" FROM "products", json_to_recordset("products"."tag_count") AS "tag_count"(tag_id bigint, count int) WHERE ((("products"."id") = ("products_0"."id"))) LIMIT ('1') :: integer) AS "tag_count_1" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_2") AS "json"FROM (SELECT "tags_2"."name" AS "name" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true')) AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/withCursor
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json", 'products_cursor', "__sj_0"."cursor") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json", CONCAT_WS(',', max("__cur_0"), max("__cur_1")) as "cursor" FROM (SELECT row_to_json("__sr_0") AS "json", "__cur_0", "__cur_1"FROM (SELECT "products_0"."name" AS "name", LAST_VALUE("products_0"."price") OVER() AS "__cur_0", LAST_VALUE("products_0"."id") OVER() AS "__cur_1" FROM (WITH "__cur" AS (SELECT a[1] as "price", a[2] as "id" FROM string_to_array('{{cursor}}', ',') as a) SELECT "products"."name", "products"."id", "products"."price" FROM "products", "__cur" WHERE (((("products"."price") < "__cur"."price" :: numeric(7,2)) OR ((("products"."price") = "__cur"."price" :: numeric(7,2)) AND (("products"."id") > "__cur"."id" :: bigint)))) ORDER BY "products"."price" DESC, "products"."id" ASC LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/nullForAuthRequiredInAnon
|
||||||
|
SELECT json_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", NULL AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/blockedQuery
|
||||||
|
SELECT json_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileQuery/blockedFunctions
|
||||||
|
SELECT json_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."email" AS "email" FROM (SELECT , "users"."email" FROM "users" WHERE (false) GROUP BY "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||||
|
--- PASS: TestCompileQuery (0.02s)
|
||||||
|
--- PASS: TestCompileQuery/withComplexArgs (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/withWhereAndList (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/withWhereIsNull (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/withWhereMultiOr (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/fetchByID (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/searchQuery (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/oneToMany (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/oneToManyReverse (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/oneToManyArray (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/manyToMany (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/manyToManyReverse (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/aggFunction (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/aggFunctionBlockedByCol (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/aggFunctionDisabled (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/aggFunctionWithFilter (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/syntheticTables (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/queryWithVariables (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/withWhereOnRelations (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/multiRoot (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/jsonColumnAsTable (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/withCursor (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/nullForAuthRequiredInAnon (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/blockedQuery (0.00s)
|
||||||
|
--- PASS: TestCompileQuery/blockedFunctions (0.00s)
|
||||||
|
=== RUN TestCompileUpdate
|
||||||
|
=== RUN TestCompileUpdate/singleUpdate
|
||||||
|
WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE ((("products"."id") = '1' :: bigint) AND (("products"."id") = '{{id}}' :: bigint)) RETURNING "products".*) SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileUpdate/simpleUpdateWithPresets
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileUpdate/nestedUpdateManyToMany
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = '{{id}}' :: bigint) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_2") AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = '{{id}}' :: bigint) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_2") AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileUpdate/nestedUpdateOneToMany
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") = '8' :: bigint) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id"= ((i.j->'product'->'where'->>'id'))::bigint) RETURNING "products".*) SELECT json_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileUpdate/nestedUpdateOneToOne
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{id}}' :: bigint) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileUpdate/nestedUpdateOneToManyWithConnect
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") = '{{id}}' :: bigint) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id"= ((i.j->'product'->'disconnect'->>'id'))::bigint) RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") SELECT json_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileUpdate/nestedUpdateOneToOneWithConnect
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint AND "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{product_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying AND "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{product_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json("__sr_1") AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||||
|
=== RUN TestCompileUpdate/nestedUpdateOneToOneWithDisconnect
|
||||||
|
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT row_to_json("__sr_0") AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||||
|
--- PASS: TestCompileUpdate (0.02s)
|
||||||
|
--- PASS: TestCompileUpdate/singleUpdate (0.00s)
|
||||||
|
--- PASS: TestCompileUpdate/simpleUpdateWithPresets (0.00s)
|
||||||
|
--- 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.175s
|
@ -222,12 +222,16 @@ func (c *compilerContext) renderDelete(qc *qcode.QCode, w io.Writer,
|
|||||||
quoted(c.w, ti.Name)
|
quoted(c.w, ti.Name)
|
||||||
io.WriteString(c.w, ` WHERE `)
|
io.WriteString(c.w, ` WHERE `)
|
||||||
|
|
||||||
|
if root.Where == nil {
|
||||||
|
return 0, errors.New("'where' clause missing in delete mutation")
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.renderWhere(root, ti); err != nil {
|
if err := c.renderWhere(root, ti); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(w, ` RETURNING `)
|
io.WriteString(w, ` RETURNING `)
|
||||||
quoted(w, ti.Name)
|
quoted(w, ti.Name)
|
||||||
io.WriteString(w, `.*)`)
|
io.WriteString(w, `.*) `)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
@ -7,26 +7,17 @@ import (
|
|||||||
|
|
||||||
func singleUpdate(t *testing.T) {
|
func singleUpdate(t *testing.T) {
|
||||||
gql := `mutation {
|
gql := `mutation {
|
||||||
product(id: 15, update: $update, where: { id: { eq: 1 } }) {
|
product(id: $id, update: $update, where: { id: { eq: 1 } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE ((("products"."id") IS NOT DISTINCT FROM 1) AND (("products"."id") = 15)) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
|
"update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
|
compileGQLToPSQL(t, gql, vars, "anon")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func simpleUpdateWithPresets(t *testing.T) {
|
func simpleUpdateWithPresets(t *testing.T) {
|
||||||
@ -36,25 +27,16 @@ func simpleUpdateWithPresets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),
|
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
compileGQLToPSQL(t, gql, vars, "user")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedUpdateManyToMany(t *testing.T) {
|
func nestedUpdateManyToMany(t *testing.T) {
|
||||||
gql := `mutation {
|
gql := `mutation {
|
||||||
purchase(update: $data, id: 5) {
|
purchase(update: $data, id: $id) {
|
||||||
sale_type
|
sale_type
|
||||||
quantity
|
quantity
|
||||||
due_date
|
due_date
|
||||||
@ -71,10 +53,6 @@ func nestedUpdateManyToMany(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(` {
|
"data": json.RawMessage(` {
|
||||||
"sale_type": "bought",
|
"sale_type": "bought",
|
||||||
@ -92,17 +70,7 @@ func nestedUpdateManyToMany(t *testing.T) {
|
|||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql1 && string(resSQL) != sql2 {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedUpdateOneToMany(t *testing.T) {
|
func nestedUpdateOneToMany(t *testing.T) {
|
||||||
@ -119,8 +87,6 @@ func nestedUpdateOneToMany(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") IS NOT DISTINCT FROM 8) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id"= ((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,19 +105,12 @@ func nestedUpdateOneToMany(t *testing.T) {
|
|||||||
}`),
|
}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedUpdateOneToOne(t *testing.T) {
|
func nestedUpdateOneToOne(t *testing.T) {
|
||||||
gql := `mutation {
|
gql := `mutation {
|
||||||
product(update: $data, id: 6) {
|
product(update: $data, id: $id) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
user {
|
user {
|
||||||
@ -162,8 +121,6 @@ func nestedUpdateOneToOne(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 6) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
"name": "Apple",
|
"name": "Apple",
|
||||||
@ -176,19 +133,13 @@ func nestedUpdateOneToOne(t *testing.T) {
|
|||||||
}`),
|
}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedUpdateOneToManyWithConnect(t *testing.T) {
|
func nestedUpdateOneToManyWithConnect(t *testing.T) {
|
||||||
gql := `mutation {
|
gql := `mutation {
|
||||||
user(update: $data, id: 6) {
|
user(update: $data, id: $id) {
|
||||||
id
|
id
|
||||||
full_name
|
full_name
|
||||||
email
|
email
|
||||||
@ -200,8 +151,6 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") = 6) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((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,19 +164,12 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) {
|
|||||||
}`),
|
}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql1 {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
||||||
gql := `mutation {
|
gql := `mutation {
|
||||||
product(update: $data, id: 9) {
|
product(update: $data, id: $product_id) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
user {
|
user {
|
||||||
@ -238,10 +180,6 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_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,29 +190,17 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
|||||||
}`),
|
}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
compileGQLToPSQL(t, gql, vars, "admin")
|
||||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(resSQL) != sql1 && string(resSQL) != sql2 {
|
|
||||||
t.Fatal(errNotExpected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
|
func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
|
||||||
gql := `mutation {
|
gql := `mutation {
|
||||||
product(update: $data, id: 2) {
|
product(update: $data, id: $id) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
user_id
|
user_id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_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) {
|
||||||
|
@ -16,8 +16,8 @@ var (
|
|||||||
type parserType int32
|
type parserType int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxFields = 100
|
maxFields = 1200
|
||||||
maxArgs = 10
|
maxArgs = 25
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -242,7 +242,8 @@ func (p *Parser) parseOp() (*Operation, error) {
|
|||||||
|
|
||||||
if p.peek(itemArgsOpen) {
|
if p.peek(itemArgsOpen) {
|
||||||
p.ignore()
|
p.ignore()
|
||||||
op.Args, err = p.parseArgs(op.Args)
|
|
||||||
|
op.Args, err = p.parseOpParams(op.Args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -338,6 +339,13 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
|
|||||||
if p.peek(itemObjOpen) {
|
if p.peek(itemObjOpen) {
|
||||||
p.ignore()
|
p.ignore()
|
||||||
st.Push(f.ID)
|
st.Push(f.ID)
|
||||||
|
|
||||||
|
} else if p.peek(itemObjClose) {
|
||||||
|
if st.Len() == 0 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +379,22 @@ func (p *Parser) parseField(f *Field) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseOpParams(args []Arg) ([]Arg, error) {
|
||||||
|
for {
|
||||||
|
if len(args) >= maxArgs {
|
||||||
|
return nil, fmt.Errorf("too many args (max %d)", maxArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.peek(itemArgsClose) {
|
||||||
|
p.ignore()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
|
func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -383,6 +407,7 @@ func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
|
|||||||
p.ignore()
|
p.ignore()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.peek(itemName) {
|
if !p.peek(itemName) {
|
||||||
return nil, errors.New("expecting an argument name")
|
return nil, errors.New("expecting an argument name")
|
||||||
}
|
}
|
||||||
@ -556,6 +581,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)
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ func TestCompile1(t *testing.T) {
|
|||||||
name
|
name
|
||||||
} }`), "user")
|
} }`), "user")
|
||||||
|
|
||||||
if err != nil {
|
if err == nil {
|
||||||
t.Fatal(err)
|
t.Fatal(errors.New("this should be an error id must be a variable"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ func TestCompile2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = qc.Compile([]byte(`
|
_, err = qc.Compile([]byte(`
|
||||||
query { product(id: 15) {
|
query { product(id: $id) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
} }`), "user")
|
} }`), "user")
|
||||||
@ -62,7 +62,7 @@ func TestCompile3(t *testing.T) {
|
|||||||
|
|
||||||
_, err = qc.Compile([]byte(`
|
_, err = qc.Compile([]byte(`
|
||||||
mutation {
|
mutation {
|
||||||
product(id: 15, name: "Test") {
|
product(id: $test, name: "Test") {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
249
qcode/qcode.go
249
qcode/qcode.go
@ -65,6 +65,7 @@ type Exp struct {
|
|||||||
Col string
|
Col string
|
||||||
NestedCols []string
|
NestedCols []string
|
||||||
Type ValType
|
Type ValType
|
||||||
|
Table string
|
||||||
Val string
|
Val string
|
||||||
ListType ValType
|
ListType ValType
|
||||||
ListVal []string
|
ListVal []string
|
||||||
@ -84,9 +85,19 @@ type OrderBy struct {
|
|||||||
Order Order
|
Order Order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PagingType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PtOffset PagingType = iota
|
||||||
|
PtForward
|
||||||
|
PtBackward
|
||||||
|
)
|
||||||
|
|
||||||
type Paging struct {
|
type Paging struct {
|
||||||
|
Type PagingType
|
||||||
Limit string
|
Limit string
|
||||||
Offset string
|
Offset string
|
||||||
|
Cursor bool
|
||||||
NoLimit bool
|
NoLimit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +131,8 @@ const (
|
|||||||
OpEqID
|
OpEqID
|
||||||
OpTsQuery
|
OpTsQuery
|
||||||
OpFalse
|
OpFalse
|
||||||
|
OpNotDistinct
|
||||||
|
OpDistinct
|
||||||
)
|
)
|
||||||
|
|
||||||
type ValType int
|
type ValType int
|
||||||
@ -132,6 +145,7 @@ const (
|
|||||||
ValList
|
ValList
|
||||||
ValVar
|
ValVar
|
||||||
ValNone
|
ValNone
|
||||||
|
ValRef
|
||||||
)
|
)
|
||||||
|
|
||||||
type AggregrateOp int
|
type AggregrateOp int
|
||||||
@ -183,6 +197,13 @@ func NewCompiler(c Config) (*Compiler, error) {
|
|||||||
return co, nil
|
return co, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewFilter() *Exp {
|
||||||
|
ex := expPool.Get().(*Exp)
|
||||||
|
ex.Reset()
|
||||||
|
|
||||||
|
return ex
|
||||||
|
}
|
||||||
|
|
||||||
func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
||||||
var err error
|
var err error
|
||||||
trv := &trval{}
|
trv := &trval{}
|
||||||
@ -343,8 +364,8 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order is important addFilters must come after compileArgs
|
// Order is important AddFilters must come after compileArgs
|
||||||
com.addFilters(qc, s, role)
|
com.AddFilters(qc, s, role)
|
||||||
|
|
||||||
if s.ParentID == -1 {
|
if s.ParentID == -1 {
|
||||||
qc.Roots = append(qc.Roots, s.ID)
|
qc.Roots = append(qc.Roots, s.ID)
|
||||||
@ -390,7 +411,7 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
func (com *Compiler) AddFilters(qc *QCode, sel *Select, role string) {
|
||||||
var fil *Exp
|
var fil *Exp
|
||||||
var nu bool
|
var nu bool
|
||||||
|
|
||||||
@ -400,10 +421,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 +435,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 {
|
||||||
@ -480,7 +500,7 @@ func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg, role string
|
|||||||
func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
|
func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
|
||||||
setActionVar := func(arg *Arg) error {
|
setActionVar := func(arg *Arg) error {
|
||||||
if arg.Val.Type != NodeVar {
|
if arg.Val.Type != NodeVar {
|
||||||
return fmt.Errorf("value for argument '%s' must be a variable", arg.Name)
|
return argErr(arg.Name, "variable")
|
||||||
}
|
}
|
||||||
qc.ActionVar = arg.Val.Val
|
qc.ActionVar = arg.Val.Val
|
||||||
return nil
|
return nil
|
||||||
@ -503,7 +523,7 @@ func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
|
|||||||
qc.Type = QTDelete
|
qc.Type = QTDelete
|
||||||
|
|
||||||
if arg.Val.Type != NodeBool {
|
if arg.Val.Type != NodeBool {
|
||||||
return fmt.Errorf("value for argument '%s' must be a boolean", arg.Name)
|
return argErr(arg.Name, "boolen")
|
||||||
}
|
}
|
||||||
|
|
||||||
if arg.Val.Val == "false" {
|
if arg.Val.Val == "false" {
|
||||||
@ -529,7 +549,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 +560,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 +597,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()
|
||||||
|
node, ok := intf.(*Node)
|
||||||
|
if !ok || node == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range node.Children {
|
||||||
|
st.Push(node.Children[i])
|
||||||
|
}
|
||||||
|
FreeNode(node, 1)
|
||||||
}
|
}
|
||||||
intf := st.Pop()
|
|
||||||
node, _ := intf.(*Node)
|
|
||||||
|
|
||||||
for i := range node.Children {
|
|
||||||
st.Push(node.Children[i])
|
|
||||||
}
|
|
||||||
nodePool.Put(node)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return root, needsUser, nil
|
return root, needsUser, nil
|
||||||
@ -603,60 +628,40 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if arg.Val.Type != NodeVar {
|
||||||
|
return argErr("id", "variable"), false
|
||||||
|
}
|
||||||
|
|
||||||
ex := expPool.Get().(*Exp)
|
ex := expPool.Get().(*Exp)
|
||||||
ex.Reset()
|
ex.Reset()
|
||||||
|
|
||||||
ex.Op = OpEqID
|
ex.Op = OpEqID
|
||||||
|
ex.Type = ValVar
|
||||||
ex.Val = arg.Val.Val
|
ex.Val = arg.Val.Val
|
||||||
|
|
||||||
switch arg.Val.Type {
|
|
||||||
case NodeStr:
|
|
||||||
ex.Type = ValStr
|
|
||||||
case NodeInt:
|
|
||||||
ex.Type = ValInt
|
|
||||||
case NodeFloat:
|
|
||||||
ex.Type = ValFloat
|
|
||||||
case NodeVar:
|
|
||||||
ex.Type = ValVar
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("expecting a string, int, float or variable"), false
|
|
||||||
}
|
|
||||||
|
|
||||||
sel.Where = ex
|
sel.Where = ex
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) {
|
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) {
|
||||||
|
if arg.Val.Type != NodeVar {
|
||||||
|
return argErr("search", "variable"), false
|
||||||
|
}
|
||||||
|
|
||||||
ex := expPool.Get().(*Exp)
|
ex := expPool.Get().(*Exp)
|
||||||
ex.Reset()
|
ex.Reset()
|
||||||
|
|
||||||
ex.Op = OpTsQuery
|
ex.Op = OpTsQuery
|
||||||
|
ex.Type = ValVar
|
||||||
ex.Val = arg.Val.Val
|
ex.Val = arg.Val.Val
|
||||||
|
|
||||||
if arg.Val.Type == NodeVar {
|
|
||||||
ex.Type = ValVar
|
|
||||||
} else {
|
|
||||||
ex.Type = ValStr
|
|
||||||
}
|
|
||||||
|
|
||||||
if sel.Args == nil {
|
if sel.Args == nil {
|
||||||
sel.Args = make(map[string]*Node)
|
sel.Args = make(map[string]*Node)
|
||||||
}
|
}
|
||||||
|
|
||||||
sel.Args[arg.Name] = arg.Val
|
sel.Args[arg.Name] = arg.Val
|
||||||
|
AddFilter(sel, ex)
|
||||||
|
|
||||||
if sel.Where != nil {
|
|
||||||
ow := sel.Where
|
|
||||||
|
|
||||||
sel.Where = expPool.Get().(*Exp)
|
|
||||||
sel.Where.Reset()
|
|
||||||
sel.Where.Op = OpAnd
|
|
||||||
sel.Where.Children = sel.Where.childrenA[:2]
|
|
||||||
sel.Where.Children[0] = ex
|
|
||||||
sel.Where.Children[1] = ow
|
|
||||||
} else {
|
|
||||||
sel.Where = ex
|
|
||||||
}
|
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -672,21 +677,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,16 +706,12 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := com.bl[node.Name]; ok {
|
if _, ok := com.bl[node.Name]; ok {
|
||||||
nodePool.Put(node)
|
FreeNode(node, 2)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == NodeObj {
|
if node.Type != NodeStr && node.Type != NodeVar {
|
||||||
for i := range node.Children {
|
return fmt.Errorf("expecting a string or variable"), false
|
||||||
st.Push(node.Children[i])
|
|
||||||
}
|
|
||||||
nodePool.Put(node)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ob := &OrderBy{}
|
ob := &OrderBy{}
|
||||||
@ -746,7 +735,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
|||||||
|
|
||||||
setOrderByColName(ob, node)
|
setOrderByColName(ob, node)
|
||||||
sel.OrderBy = append(sel.OrderBy, ob)
|
sel.OrderBy = append(sel.OrderBy, ob)
|
||||||
nodePool.Put(node)
|
FreeNode(node, 3)
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@ -768,7 +757,7 @@ func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) (error, bool) {
|
|||||||
|
|
||||||
for i := range node.Children {
|
for i := range node.Children {
|
||||||
sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val)
|
sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val)
|
||||||
nodePool.Put(node.Children[i])
|
FreeNode(node.Children[i], 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
@ -778,7 +767,7 @@ func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) (error, bool) {
|
|||||||
node := arg.Val
|
node := arg.Val
|
||||||
|
|
||||||
if node.Type != NodeInt {
|
if node.Type != NodeInt {
|
||||||
return fmt.Errorf("expecting an integer"), false
|
return argErr("limit", "number"), false
|
||||||
}
|
}
|
||||||
|
|
||||||
sel.Paging.Limit = node.Val
|
sel.Paging.Limit = node.Val
|
||||||
@ -789,14 +778,39 @@ func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) (error, bool) {
|
|||||||
func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) {
|
func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) {
|
||||||
node := arg.Val
|
node := arg.Val
|
||||||
|
|
||||||
if node.Type != NodeInt {
|
if node.Type != NodeVar {
|
||||||
return fmt.Errorf("expecting an integer"), false
|
return argErr("offset", "variable"), false
|
||||||
}
|
}
|
||||||
|
|
||||||
sel.Paging.Offset = node.Val
|
sel.Paging.Offset = node.Val
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (com *Compiler) compileArgFirstLast(sel *Select, arg *Arg, pt PagingType) (error, bool) {
|
||||||
|
node := arg.Val
|
||||||
|
|
||||||
|
if node.Type != NodeInt {
|
||||||
|
return argErr(arg.Name, "number"), false
|
||||||
|
}
|
||||||
|
|
||||||
|
sel.Paging.Type = pt
|
||||||
|
sel.Paging.Limit = node.Val
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (com *Compiler) compileArgAfterBefore(sel *Select, arg *Arg, pt PagingType) (error, bool) {
|
||||||
|
node := arg.Val
|
||||||
|
|
||||||
|
if node.Type != NodeVar || node.Val != "cursor" {
|
||||||
|
return fmt.Errorf("value for argument '%s' must be a variable named $cursor", arg.Name), false
|
||||||
|
}
|
||||||
|
sel.Paging.Type = pt
|
||||||
|
sel.Paging.Cursor = true
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
var zeroTrv = &trval{}
|
var zeroTrv = &trval{}
|
||||||
|
|
||||||
func (com *Compiler) getRole(role, field string) *trval {
|
func (com *Compiler) getRole(role, field string) *trval {
|
||||||
@ -807,6 +821,27 @@ func (com *Compiler) getRole(role, field string) *trval {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddFilter(sel *Select, fil *Exp) {
|
||||||
|
if sel.Where != nil {
|
||||||
|
ow := sel.Where
|
||||||
|
|
||||||
|
if sel.Where.Op != OpAnd || !sel.Where.doFree {
|
||||||
|
sel.Where = expPool.Get().(*Exp)
|
||||||
|
sel.Where.Reset()
|
||||||
|
sel.Where.Op = OpAnd
|
||||||
|
sel.Where.Children = sel.Where.childrenA[:2]
|
||||||
|
sel.Where.Children[0] = fil
|
||||||
|
sel.Where.Children[1] = ow
|
||||||
|
|
||||||
|
} else {
|
||||||
|
sel.Where.Children = append(sel.Where.Children, fil)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
sel.Where = fil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
||||||
name := node.Name
|
name := node.Name
|
||||||
if name[0] == '_' {
|
if name[0] == '_' {
|
||||||
@ -821,6 +856,7 @@ func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
|||||||
} else {
|
} else {
|
||||||
ex = &Exp{doFree: false}
|
ex = &Exp{doFree: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
ex.Children = ex.childrenA[:0]
|
ex.Children = ex.childrenA[:0]
|
||||||
|
|
||||||
switch name {
|
switch name {
|
||||||
@ -902,6 +938,12 @@ func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
|||||||
case "is_null":
|
case "is_null":
|
||||||
ex.Op = OpIsNull
|
ex.Op = OpIsNull
|
||||||
ex.Val = node.Val
|
ex.Val = node.Val
|
||||||
|
case "null_eq", "ndis", "not_distinct":
|
||||||
|
ex.Op = OpNotDistinct
|
||||||
|
ex.Val = node.Val
|
||||||
|
case "null_neq", "dis", "distinct":
|
||||||
|
ex.Op = OpDistinct
|
||||||
|
ex.Val = node.Val
|
||||||
default:
|
default:
|
||||||
pushChildren(st, node.exp, node)
|
pushChildren(st, node.exp, node)
|
||||||
return nil, nil // skip node
|
return nil, nil // skip node
|
||||||
@ -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) {
|
||||||
@ -1130,3 +1171,7 @@ func FreeExp(ex *Exp) {
|
|||||||
expPool.Put(ex)
|
expPool.Put(ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func argErr(name, ty string) error {
|
||||||
|
return fmt.Errorf("value for argument '%s' must be a %s", name, ty)
|
||||||
|
}
|
||||||
|
@ -4,8 +4,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Masterminds/semver"
|
|
||||||
"github.com/adjust/gorails/marshal"
|
"github.com/adjust/gorails/marshal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -37,17 +38,20 @@ func NewAuth(version, secret string) (*Auth, error) {
|
|||||||
AuthSalt: authSalt,
|
AuthSalt: authSalt,
|
||||||
}
|
}
|
||||||
|
|
||||||
ver, err := semver.NewVersion(version)
|
var v1, v2 int
|
||||||
if err != nil {
|
var err error
|
||||||
return nil, fmt.Errorf("rails auth: %s", err)
|
|
||||||
|
sv := strings.Split(version, ".")
|
||||||
|
if len(sv) >= 2 {
|
||||||
|
if v1, err = strconv.Atoi(sv[0]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if v2, err = strconv.Atoi(sv[1]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gt52, err := semver.NewConstraint(">= 5.2")
|
if v1 >= 5 && v2 >= 2 {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("rails auth: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if gt52.Check(ver) {
|
|
||||||
ra.Cipher = railsCipher52
|
ra.Cipher = railsCipher52
|
||||||
} else {
|
} else {
|
||||||
ra.Cipher = railsCipher
|
ra.Cipher = railsCipher
|
||||||
|
13
scripts/start.sh
Executable file
13
scripts/start.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
if [ $1 = "secrets" ]
|
||||||
|
then
|
||||||
|
./sops --config ./config "${@:2}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -f "./config/$SECRETS_FILE"
|
||||||
|
then
|
||||||
|
./sops --config ./config exec-env "./config/$SECRETS_FILE" "$*"
|
||||||
|
else
|
||||||
|
$@
|
||||||
|
fi
|
49
serv/args.go
49
serv/args.go
@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
@ -18,31 +17,46 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
|
|||||||
if v := ctx.Value(userIDProviderKey); v != nil {
|
if v := ctx.Value(userIDProviderKey); v != nil {
|
||||||
return io.WriteString(w, v.(string))
|
return io.WriteString(w, v.(string))
|
||||||
}
|
}
|
||||||
return 0, errors.New("query requires variable $user_id_provider")
|
return 0, argErr("user_id_provider")
|
||||||
|
|
||||||
case "user_id":
|
case "user_id":
|
||||||
if v := ctx.Value(userIDKey); v != nil {
|
if v := ctx.Value(userIDKey); v != nil {
|
||||||
return io.WriteString(w, v.(string))
|
return io.WriteString(w, v.(string))
|
||||||
}
|
}
|
||||||
return 0, errors.New("query requires variable $user_id")
|
return 0, argErr("user_id")
|
||||||
|
|
||||||
case "user_role":
|
case "user_role":
|
||||||
if v := ctx.Value(userRoleKey); v != nil {
|
if v := ctx.Value(userRoleKey); v != nil {
|
||||||
return io.WriteString(w, v.(string))
|
return io.WriteString(w, v.(string))
|
||||||
}
|
}
|
||||||
return 0, errors.New("query requires variable $user_role")
|
return 0, argErr("user_role")
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
|
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
|
||||||
|
|
||||||
if len(fields) == 0 {
|
if len(fields) == 0 {
|
||||||
return 0, nil
|
return 0, argErr(tag)
|
||||||
|
|
||||||
}
|
}
|
||||||
v := fields[0].Value
|
v := fields[0].Value
|
||||||
|
|
||||||
|
// Open and close quotes
|
||||||
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
||||||
fields[0].Value = v[1 : len(v)-1]
|
fields[0].Value = v[1 : len(v)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tag == "cursor" {
|
||||||
|
if bytes.EqualFold(v, []byte("null")) {
|
||||||
|
return io.WriteString(w, ``)
|
||||||
|
}
|
||||||
|
v1, err := decrypt(string(fields[0].Value))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Write(v1)
|
||||||
|
}
|
||||||
|
|
||||||
return w.Write(escQuote(fields[0].Value))
|
return w.Write(escQuote(fields[0].Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,27 +77,37 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
|||||||
|
|
||||||
for i := range args {
|
for i := range args {
|
||||||
av := args[i]
|
av := args[i]
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case bytes.Equal(av, []byte("user_id")):
|
case bytes.Equal(av, []byte("user_id")):
|
||||||
if v := ctx.Value(userIDKey); v != nil {
|
if v := ctx.Value(userIDKey); v != nil {
|
||||||
vars[i] = v.(string)
|
vars[i] = v.(string)
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("query requires variable $user_id")
|
return nil, argErr("user_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
case bytes.Equal(av, []byte("user_id_provider")):
|
case bytes.Equal(av, []byte("user_id_provider")):
|
||||||
if v := ctx.Value(userIDProviderKey); v != nil {
|
if v := ctx.Value(userIDProviderKey); v != nil {
|
||||||
vars[i] = v.(string)
|
vars[i] = v.(string)
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("query requires variable $user_id_provider")
|
return nil, argErr("user_id_provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
case bytes.Equal(av, []byte("user_role")):
|
case bytes.Equal(av, []byte("user_role")):
|
||||||
if v := ctx.Value(userRoleKey); v != nil {
|
if v := ctx.Value(userRoleKey); v != nil {
|
||||||
vars[i] = v.(string)
|
vars[i] = v.(string)
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("query requires variable $user_role")
|
return nil, argErr("user_role")
|
||||||
|
}
|
||||||
|
|
||||||
|
case bytes.Equal(av, []byte("cursor")):
|
||||||
|
if v, ok := fields["cursor"]; ok && v[0] == '"' {
|
||||||
|
v1, err := decrypt(string(v[1 : len(v)-1]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vars[i] = v1
|
||||||
|
} else {
|
||||||
|
return nil, argErr("cursor")
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -96,11 +120,12 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
|||||||
if err := json.Unmarshal(v, &val); err != nil {
|
if err := json.Unmarshal(v, &val); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
vars[i] = val
|
vars[i] = val
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("query requires variable $%s", string(av))
|
return nil, argErr(string(av))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,3 +160,7 @@ func escQuote(b []byte) []byte {
|
|||||||
}
|
}
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func argErr(name string) error {
|
||||||
|
return fmt.Errorf("query requires variable '%s' to be set", name)
|
||||||
|
}
|
||||||
|
20
serv/cmd.go
20
serv/cmd.go
@ -29,15 +29,17 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger zerolog.Logger // logger for everything but errors
|
logger zerolog.Logger // logger for everything but errors
|
||||||
errlog zerolog.Logger // logger for errors includes line numbers
|
errlog zerolog.Logger // logger for errors includes line numbers
|
||||||
conf *config // parsed config
|
conf *config // parsed config
|
||||||
confPath string // path to the config file
|
confPath string // path to the config file
|
||||||
db *pgxpool.Pool // database connection pool
|
db *pgxpool.Pool // database connection pool
|
||||||
schema *psql.DBSchema // database tables, columns and relationships
|
schema *psql.DBSchema // database tables, columns and relationships
|
||||||
allowList *allow.List // allow.list is contains queries allowed in production
|
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() {
|
||||||
|
120
serv/cmd_seed.go
120
serv/cmd_seed.go
@ -3,15 +3,20 @@ package serv
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/brianvoe/gofakeit"
|
"github.com/brianvoe/gofakeit"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/valyala/fasttemplate"
|
"github.com/valyala/fasttemplate"
|
||||||
)
|
)
|
||||||
@ -41,6 +46,7 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
vm.Set("graphql", graphQLFunc)
|
vm.Set("graphql", graphQLFunc)
|
||||||
|
vm.Set("import_csv", importCSV)
|
||||||
|
|
||||||
console := vm.NewObject()
|
console := vm.NewObject()
|
||||||
console.Set("log", logFunc) //nolint: errcheck
|
console.Set("log", logFunc) //nolint: errcheck
|
||||||
@ -128,6 +134,106 @@ func graphQLFunc(query string, data interface{}, opt map[string]string) map[stri
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type csvSource struct {
|
||||||
|
rows [][]string
|
||||||
|
i int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCSVSource(filename string) (*csvSource, error) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
r := csv.NewReader(f)
|
||||||
|
rows, err := r.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &csvSource{rows: rows}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *csvSource) Next() bool {
|
||||||
|
return c.i < len(c.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *csvSource) Values() ([]interface{}, error) {
|
||||||
|
var vals []interface{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, v := range c.rows[c.i] {
|
||||||
|
switch {
|
||||||
|
case len(v) == 0:
|
||||||
|
vals = append(vals, "")
|
||||||
|
case isDigit(v):
|
||||||
|
var n int
|
||||||
|
if n, err = strconv.Atoi(v); err == nil {
|
||||||
|
vals = append(vals, n)
|
||||||
|
}
|
||||||
|
case strings.EqualFold(v, "true") || strings.EqualFold(v, "false"):
|
||||||
|
var b bool
|
||||||
|
if b, err = strconv.ParseBool(v); err == nil {
|
||||||
|
vals = append(vals, b)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
vals = append(vals, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w (line no %d)", err, c.i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.i++
|
||||||
|
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDigit(v string) bool {
|
||||||
|
for i := range v {
|
||||||
|
if v[i] < '0' || v[i] > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *csvSource) Err() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func importCSV(table, filename string) int64 {
|
||||||
|
if filename[0] != '/' {
|
||||||
|
filename = path.Join(confPath, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := NewCSVSource(filename)
|
||||||
|
if err != nil {
|
||||||
|
errlog.Fatal().Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
var cols []string
|
||||||
|
colval, _ := s.Values()
|
||||||
|
|
||||||
|
for _, c := range colval {
|
||||||
|
cols = append(cols, c.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := db.CopyFrom(
|
||||||
|
context.Background(),
|
||||||
|
pgx.Identifier{table},
|
||||||
|
cols,
|
||||||
|
s)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("%w (line no %d)", err, s.i)
|
||||||
|
errlog.Fatal().Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
//nolint: errcheck
|
//nolint: errcheck
|
||||||
func logFunc(args ...interface{}) {
|
func logFunc(args ...interface{}) {
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
@ -145,6 +251,17 @@ func logFunc(args ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func avatarURL(size int) string {
|
||||||
|
if size == 0 {
|
||||||
|
size = 200
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("https://i.pravatar.cc/%d?%d", size, rand.Intn(5000))
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageURL(width int, height int) string {
|
||||||
|
return fmt.Sprintf("https://picsum.photos/%d/%d?%d", width, height, rand.Intn(5000))
|
||||||
|
}
|
||||||
|
|
||||||
//nolint: errcheck
|
//nolint: errcheck
|
||||||
func setFakeFuncs(f *goja.Object) {
|
func setFakeFuncs(f *goja.Object) {
|
||||||
gofakeit.Seed(0)
|
gofakeit.Seed(0)
|
||||||
@ -222,7 +339,8 @@ func setFakeFuncs(f *goja.Object) {
|
|||||||
|
|
||||||
// Internet
|
// Internet
|
||||||
f.Set("url", gofakeit.URL)
|
f.Set("url", gofakeit.URL)
|
||||||
f.Set("image_url", gofakeit.ImageURL)
|
f.Set("image_url", imageURL)
|
||||||
|
f.Set("avatar_url", avatarURL)
|
||||||
f.Set("domain_name", gofakeit.DomainName)
|
f.Set("domain_name", gofakeit.DomainName)
|
||||||
f.Set("domain_suffix", gofakeit.DomainSuffix)
|
f.Set("domain_suffix", gofakeit.DomainSuffix)
|
||||||
f.Set("ipv4_address", gofakeit.IPv4Address)
|
f.Set("ipv4_address", gofakeit.IPv4Address)
|
||||||
|
@ -19,10 +19,13 @@ func cmdServ(cmd *cobra.Command, args []string) {
|
|||||||
fatalInProd(err, "failed to connect to database")
|
fatalInProd(err, "failed to connect to database")
|
||||||
}
|
}
|
||||||
|
|
||||||
initCompiler()
|
if conf != nil && db != nil {
|
||||||
initResolvers()
|
initCrypto()
|
||||||
initAllowList(confPath)
|
initCompiler()
|
||||||
initPreparedList(confPath)
|
initResolvers()
|
||||||
|
initAllowList(confPath)
|
||||||
|
initPreparedList(confPath)
|
||||||
|
}
|
||||||
|
|
||||||
startHTTP()
|
startHTTP()
|
||||||
}
|
}
|
||||||
|
@ -26,10 +26,13 @@ type config struct {
|
|||||||
EnableTracing bool `mapstructure:"enable_tracing"`
|
EnableTracing bool `mapstructure:"enable_tracing"`
|
||||||
UseAllowList bool `mapstructure:"use_allow_list"`
|
UseAllowList bool `mapstructure:"use_allow_list"`
|
||||||
Production bool
|
Production bool
|
||||||
WatchAndReload bool `mapstructure:"reload_on_config_change"`
|
WatchAndReload bool `mapstructure:"reload_on_config_change"`
|
||||||
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"`
|
||||||
|
AllowedOrigins []string `mapstructure:"cors_allowed_origins"`
|
||||||
|
DebugCORS bool `mapstructure:"cors_debug"`
|
||||||
|
|
||||||
Inflections map[string]string
|
Inflections map[string]string
|
||||||
|
|
||||||
@ -204,8 +207,8 @@ func newConfig(name string) *viper.Viper {
|
|||||||
vi.SetDefault("env", "development")
|
vi.SetDefault("env", "development")
|
||||||
|
|
||||||
vi.BindEnv("env", "GO_ENV") //nolint: errcheck
|
vi.BindEnv("env", "GO_ENV") //nolint: errcheck
|
||||||
vi.BindEnv("HOST", "HOST") //nolint: errcheck
|
vi.BindEnv("host", "HOST") //nolint: errcheck
|
||||||
vi.BindEnv("PORT", "PORT") //nolint: errcheck
|
vi.BindEnv("port", "PORT") //nolint: errcheck
|
||||||
|
|
||||||
vi.SetDefault("auth.rails.max_idle", 80)
|
vi.SetDefault("auth.rails.max_idle", 80)
|
||||||
vi.SetDefault("auth.rails.max_active", 12000)
|
vi.SetDefault("auth.rails.max_active", 12000)
|
||||||
|
13
serv/core.go
13
serv/core.go
@ -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"
|
||||||
)
|
)
|
||||||
@ -151,6 +152,10 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *stmt, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if root, err = encryptCursor(ps.st.qc, root); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return root, &ps.st, nil
|
return root, &ps.st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,6 +197,8 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
|
|||||||
}
|
}
|
||||||
st := &stmts[0]
|
st := &stmts[0]
|
||||||
|
|
||||||
|
//fmt.Println(">", string(st.sql))
|
||||||
|
|
||||||
t := fasttemplate.New(st.sql, openVar, closeVar)
|
t := fasttemplate.New(st.sql, openVar, closeVar)
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
@ -241,8 +248,12 @@ 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.Add(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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,12 +59,6 @@ func buildRoleStmt(gql, vars []byte, role string) ([]stmt, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// For the 'anon' role in production only compile
|
|
||||||
// queries for tables defined in the config file.
|
|
||||||
if conf.Production && ro.Name == "anon" && !hasTablesWithConfig(qc, ro) {
|
|
||||||
return nil, errors.New("query contains tables with no 'anon' role config")
|
|
||||||
}
|
|
||||||
|
|
||||||
stmts := []stmt{stmt{role: ro, qc: qc}}
|
stmts := []stmt{stmt{role: ro, qc: qc}}
|
||||||
w := &bytes.Buffer{}
|
w := &bytes.Buffer{}
|
||||||
|
|
||||||
@ -90,7 +84,7 @@ func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(conf.RolesQuery) == 0 {
|
if len(conf.RolesQuery) == 0 {
|
||||||
return buildRoleStmt(gql, vars, "user")
|
return nil, errors.New("roles_query not defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmts := make([]stmt, 0, len(conf.Roles))
|
stmts := make([]stmt, 0, len(conf.Roles))
|
||||||
@ -99,6 +93,7 @@ func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
|
|||||||
for i := 0; i < len(conf.Roles); i++ {
|
for i := 0; i < len(conf.Roles); i++ {
|
||||||
role := &conf.Roles[i]
|
role := &conf.Roles[i]
|
||||||
|
|
||||||
|
// skip anon as it's not included in the combined multi-statement
|
||||||
if role.Name == "anon" {
|
if role.Name == "anon" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
72
serv/cursor.go
Normal file
72
serv/cursor.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package serv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/dosco/super-graph/crypto"
|
||||||
|
"github.com/dosco/super-graph/jsn"
|
||||||
|
"github.com/dosco/super-graph/qcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) {
|
||||||
|
var keys [][]byte
|
||||||
|
|
||||||
|
for _, s := range qc.Selects {
|
||||||
|
if s.Paging.Type != qcode.PtOffset {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
buf.WriteString(s.FieldName)
|
||||||
|
buf.WriteString("_cursor")
|
||||||
|
keys = append(keys, buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
from := jsn.Get(data, keys)
|
||||||
|
to := make([]jsn.Field, len(from))
|
||||||
|
|
||||||
|
for i, f := range from {
|
||||||
|
to[i].Key = f.Key
|
||||||
|
|
||||||
|
if f.Value[0] != '"' || f.Value[len(f.Value)-1] != '"' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
if len(f.Value) > 2 {
|
||||||
|
v, err := crypto.Encrypt(f.Value[1:len(f.Value)-1], &internalKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteByte('"')
|
||||||
|
buf.WriteString(base64.StdEncoding.EncodeToString(v))
|
||||||
|
buf.WriteByte('"')
|
||||||
|
} else {
|
||||||
|
buf.WriteString(`null`)
|
||||||
|
}
|
||||||
|
|
||||||
|
to[i].Value = buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
if err := jsn.Replace(&buf, data, from, to); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrypt(data string) ([]byte, error) {
|
||||||
|
v, err := base64.StdEncoding.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return crypto.Decrypt(v, &internalKey)
|
||||||
|
}
|
18
serv/http.go
18
serv/http.go
@ -8,6 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/cors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -61,6 +63,20 @@ type resolver struct {
|
|||||||
Duration time.Duration `json:"duration"`
|
Duration time.Duration `json:"duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiV1Handler() http.Handler {
|
||||||
|
h := withAuth(http.HandlerFunc(apiV1), conf.Auth)
|
||||||
|
|
||||||
|
if len(conf.AllowedOrigins) != 0 {
|
||||||
|
c := cors.New(cors.Options{
|
||||||
|
AllowedOrigins: conf.AllowedOrigins,
|
||||||
|
AllowCredentials: true,
|
||||||
|
Debug: conf.DebugCORS,
|
||||||
|
})
|
||||||
|
h = c.Handler(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
func apiV1(w http.ResponseWriter, r *http.Request) {
|
func apiV1(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := &coreContext{Context: r.Context()}
|
ctx := &coreContext{Context: r.Context()}
|
||||||
|
|
||||||
@ -101,7 +117,7 @@ func apiV1(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errlog.Error().Err(err).Msg("failed to handle request")
|
errlog.Error().Err(err).Msg(ctx.req.Query)
|
||||||
errorResp(w, err)
|
errorResp(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
26
serv/init.go
26
serv/init.go
@ -2,10 +2,13 @@ package serv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
@ -133,7 +136,17 @@ func initDBPool(c *config) (*pgxpool.Pool, error) {
|
|||||||
config.MaxConns = conf.DB.PoolSize
|
config.MaxConns = conf.DB.PoolSize
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := pgxpool.ConnectConfig(context.Background(), config)
|
var db *pgxpool.Pool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
db, err = pgxpool.ConnectConfig(context.Background(), config)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(i*100) * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -163,3 +176,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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/dosco/super-graph/allow"
|
"github.com/dosco/super-graph/allow"
|
||||||
|
"github.com/dosco/super-graph/psql"
|
||||||
"github.com/dosco/super-graph/qcode"
|
"github.com/dosco/super-graph/qcode"
|
||||||
"github.com/jackc/pgconn"
|
"github.com/jackc/pgconn"
|
||||||
"github.com/jackc/pgx/v4"
|
"github.com/jackc/pgx/v4"
|
||||||
@ -82,12 +83,6 @@ func prepareStmt(item allow.Item) error {
|
|||||||
qt := qcode.GetQType(gql)
|
qt := qcode.GetQType(gql)
|
||||||
q := []byte(gql)
|
q := []byte(gql)
|
||||||
|
|
||||||
if len(vars) == 0 {
|
|
||||||
logger.Debug().Msgf("Prepared statement:\n%s\n", gql)
|
|
||||||
} else {
|
|
||||||
logger.Debug().Msgf("Prepared statement:\n%s\n%s\n", vars, gql)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := db.Begin(context.Background())
|
tx, err := db.Begin(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -109,7 +104,7 @@ func prepareStmt(item allow.Item) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug().Msg("Prepared statement role: user")
|
logger.Debug().Msgf("Prepared statement 'query %s' (user)", item.Name)
|
||||||
|
|
||||||
err = prepare(tx, stmts1, stmtHash(item.Name, "user"))
|
err = prepare(tx, stmts1, stmtHash(item.Name, "user"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -117,9 +112,12 @@ func prepareStmt(item allow.Item) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if conf.isAnonRoleDefined() {
|
if conf.isAnonRoleDefined() {
|
||||||
logger.Debug().Msg("Prepared statement for role: anon")
|
logger.Debug().Msgf("Prepared statement 'query %s' (anon)", item.Name)
|
||||||
|
|
||||||
stmts2, err := buildRoleStmt(q, vars, "anon")
|
stmts2, err := buildRoleStmt(q, vars, "anon")
|
||||||
|
if err == psql.ErrAllTablesSkipped {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -132,11 +130,17 @@ func prepareStmt(item allow.Item) error {
|
|||||||
|
|
||||||
case qcode.QTMutation:
|
case qcode.QTMutation:
|
||||||
for _, role := range conf.Roles {
|
for _, role := range conf.Roles {
|
||||||
logger.Debug().Msgf("Prepared statement for role: %s", role.Name)
|
logger.Debug().Msgf("Prepared statement 'mutation %s' (%s)", item.Name, role.Name)
|
||||||
|
|
||||||
stmts, err := buildRoleStmt(q, vars, role.Name)
|
stmts, err := buildRoleStmt(q, vars, role.Name)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if len(item.Vars) == 0 {
|
||||||
|
logger.Warn().Err(err).Msg(item.Query)
|
||||||
|
} else {
|
||||||
|
logger.Warn().Err(err).Msgf("%s %s", item.Vars, item.Query)
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = prepare(tx, stmts, stmtHash(item.Name, role.Name))
|
err = prepare(tx, stmts, stmtHash(item.Name, role.Name))
|
||||||
|
@ -108,7 +108,11 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
|
|||||||
// Ensure that we use the correct events, as they are not uniform across
|
// Ensure that we use the correct events, as they are not uniform across
|
||||||
// platforms. See https://github.com/fsnotify/fsnotify/issues/74
|
// platforms. See https://github.com/fsnotify/fsnotify/issues/74
|
||||||
|
|
||||||
if conf != nil && !conf.Production && strings.HasSuffix(event.Name, "/allow.list") {
|
if conf != nil && strings.HasSuffix(event.Name, "/allow.list") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Production {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
502
serv/rice-box.go
502
serv/rice-box.go
File diff suppressed because one or more lines are too long
@ -154,7 +154,7 @@ func routeHandler() (http.Handler, error) {
|
|||||||
|
|
||||||
routes := map[string]http.Handler{
|
routes := map[string]http.Handler{
|
||||||
"/health": http.HandlerFunc(health),
|
"/health": http.HandlerFunc(health),
|
||||||
"/api/v1/graphql": withAuth(http.HandlerFunc(apiV1), conf.Auth),
|
"/api/v1/graphql": apiV1Handler(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setActionRoutes(routes); err != nil {
|
if err := setActionRoutes(routes); err != nil {
|
||||||
|
47
tmpl/dev.yml
47
tmpl/dev.yml
@ -32,6 +32,19 @@ 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
|
||||||
|
|
||||||
|
# CORS: A list of origins a cross-domain request can be executed from.
|
||||||
|
# If the special * value is present in the list, all origins will be allowed.
|
||||||
|
# An origin may contain a wildcard (*) to replace 0 or more
|
||||||
|
# characters (i.e.: http://*.domain.com).
|
||||||
|
cors_allowed_origins: ["*"]
|
||||||
|
|
||||||
|
# Debug Cross Origin Resource Sharing requests
|
||||||
|
cors_debug: false
|
||||||
|
|
||||||
# Postgres related environment Variables
|
# Postgres related environment Variables
|
||||||
# SG_DATABASE_HOST
|
# SG_DATABASE_HOST
|
||||||
# SG_DATABASE_PORT
|
# SG_DATABASE_PORT
|
||||||
@ -105,7 +118,7 @@ database:
|
|||||||
port: 5432
|
port: 5432
|
||||||
dbname: {% app_name_slug %}_development
|
dbname: {% app_name_slug %}_development
|
||||||
user: postgres
|
user: postgres
|
||||||
password: ''
|
password: postgres
|
||||||
|
|
||||||
#schema: "public"
|
#schema: "public"
|
||||||
#pool_size: 10
|
#pool_size: 10
|
||||||
@ -121,7 +134,9 @@ database:
|
|||||||
|
|
||||||
# Define additional variables here to be used with filters
|
# Define additional variables here to be used with filters
|
||||||
variables:
|
variables:
|
||||||
admin_account_id: "5"
|
#admin_account_id: "5"
|
||||||
|
admin_account_id: "sql:select id from users where admin = true limit 1"
|
||||||
|
|
||||||
|
|
||||||
# Field and table names that you wish to block
|
# Field and table names that you wish to block
|
||||||
blocklist:
|
blocklist:
|
||||||
@ -164,26 +179,14 @@ tables:
|
|||||||
table: users
|
table: users
|
||||||
|
|
||||||
|
|
||||||
roles_query: "SELECT * FROM users WHERE id = $user_id"
|
#roles_query: "SELECT * FROM users WHERE id = $user_id"
|
||||||
|
|
||||||
roles:
|
roles:
|
||||||
- name: anon
|
- name: anon
|
||||||
tables:
|
tables:
|
||||||
- name: products
|
- name: users
|
||||||
limit: 10
|
|
||||||
|
|
||||||
query:
|
query:
|
||||||
columns: ["id", "name", "description" ]
|
limit: 10
|
||||||
aggregation: false
|
|
||||||
|
|
||||||
insert:
|
|
||||||
block: false
|
|
||||||
|
|
||||||
update:
|
|
||||||
block: false
|
|
||||||
|
|
||||||
delete:
|
|
||||||
block: false
|
|
||||||
|
|
||||||
- name: user
|
- name: user
|
||||||
tables:
|
tables:
|
||||||
@ -211,8 +214,8 @@ roles:
|
|||||||
delete:
|
delete:
|
||||||
block: true
|
block: true
|
||||||
|
|
||||||
- name: admin
|
# - name: admin
|
||||||
match: id = 1000
|
# match: id = 1000
|
||||||
tables:
|
# tables:
|
||||||
- name: users
|
# - name: users
|
||||||
filters: []
|
# filters: []
|
||||||
|
@ -1,14 +1,56 @@
|
|||||||
version: '3.4'
|
version: '3.4'
|
||||||
services:
|
services:
|
||||||
|
# Postgres DB
|
||||||
db:
|
db:
|
||||||
image: postgres
|
image: postgres:12
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
|
||||||
|
# Yugabyte DB
|
||||||
|
# yb-master:
|
||||||
|
# image: yugabytedb/yugabyte:latest
|
||||||
|
# container_name: yb-master-n1
|
||||||
|
# command: [ "/home/yugabyte/bin/yb-master",
|
||||||
|
# "--fs_data_dirs=/mnt/disk0,/mnt/disk1",
|
||||||
|
# "--master_addresses=yb-master-n1:7100",
|
||||||
|
# "--replication_factor=1",
|
||||||
|
# "--enable_ysql=true"]
|
||||||
|
# ports:
|
||||||
|
# - "7000:7000"
|
||||||
|
# environment:
|
||||||
|
# SERVICE_7000_NAME: yb-master
|
||||||
|
|
||||||
|
# db:
|
||||||
|
# image: yugabytedb/yugabyte:latest
|
||||||
|
# container_name: yb-tserver-n1
|
||||||
|
# command: [ "/home/yugabyte/bin/yb-tserver",
|
||||||
|
# "--fs_data_dirs=/mnt/disk0,/mnt/disk1",
|
||||||
|
# "--start_pgsql_proxy",
|
||||||
|
# "--tserver_master_addrs=yb-master-n1:7100"]
|
||||||
|
# ports:
|
||||||
|
# - "9042:9042"
|
||||||
|
# - "6379:6379"
|
||||||
|
# - "5433:5433"
|
||||||
|
# - "9000:9000"
|
||||||
|
# environment:
|
||||||
|
# SERVICE_5433_NAME: ysql
|
||||||
|
# SERVICE_9042_NAME: ycql
|
||||||
|
# SERVICE_6379_NAME: yedis
|
||||||
|
# SERVICE_9000_NAME: yb-tserver
|
||||||
|
# depends_on:
|
||||||
|
# - yb-master
|
||||||
|
|
||||||
{% app_name_slug %}_api:
|
{% app_name_slug %}_api:
|
||||||
image: dosco/super-graph:latest
|
image: dosco/super-graph:latest
|
||||||
environment:
|
environment:
|
||||||
GO_ENV: "development"
|
GO_ENV: "development"
|
||||||
|
# Uncomment below for Yugabyte DB
|
||||||
|
# SG_DATABASE_PORT: 5433
|
||||||
|
# SG_DATABASE_USER: yugabyte
|
||||||
|
# SG_DATABASE_PASSWORD: yugabyte
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/config
|
- ./config:/config
|
||||||
ports:
|
ports:
|
||||||
|
@ -24,7 +24,11 @@ auth_fail_block: true
|
|||||||
# Latency tracing for database queries and remote joins
|
# Latency tracing for database queries and remote joins
|
||||||
# the resulting latency information is returned with the
|
# the resulting latency information is returned with the
|
||||||
# response
|
# response
|
||||||
enable_tracing: true
|
enable_tracing: false
|
||||||
|
|
||||||
|
# Watch the config folder and reload Super Graph
|
||||||
|
# with the new configs when a change is detected
|
||||||
|
reload_on_config_change: false
|
||||||
|
|
||||||
# File that points to the database seeding script
|
# File that points to the database seeding script
|
||||||
# seed_file: seed.js
|
# seed_file: seed.js
|
||||||
@ -32,6 +36,19 @@ 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
|
||||||
|
|
||||||
|
# CORS: A list of origins a cross-domain request can be executed from.
|
||||||
|
# If the special * value is present in the list, all origins will be allowed.
|
||||||
|
# An origin may contain a wildcard (*) to replace 0 or more
|
||||||
|
# characters (i.e.: http://*.domain.com).
|
||||||
|
# cors_allowed_origins: ["*"]
|
||||||
|
|
||||||
|
# Debug Cross Origin Resource Sharing requests
|
||||||
|
# cors_debug: false
|
||||||
|
|
||||||
# Postgres related environment Variables
|
# Postgres related environment Variables
|
||||||
# SG_DATABASE_HOST
|
# SG_DATABASE_HOST
|
||||||
# SG_DATABASE_PORT
|
# SG_DATABASE_PORT
|
||||||
@ -48,9 +65,9 @@ database:
|
|||||||
type: postgres
|
type: postgres
|
||||||
host: db
|
host: db
|
||||||
port: 5432
|
port: 5432
|
||||||
dbname: {% app_name_slug %}_development
|
dbname: {% app_name_slug %}_production
|
||||||
user: postgres
|
user: postgres
|
||||||
password: ''
|
password: postgres
|
||||||
#pool_size: 10
|
#pool_size: 10
|
||||||
#max_retries: 0
|
#max_retries: 0
|
||||||
#log_level: "debug"
|
#log_level: "debug"
|
||||||
|
Reference in New Issue
Block a user