Skip to content

Commit bde6281

Browse files
authored
feat: Upload projects (#1436)
`sqlc upload` will package up schema, queries and output code and upload it to sqlc Cloud. Uploaded projects are used to verify future sqlc releases; all uploaded code is tested for backwards-incompaitble changes. * bundler: Add flag to dump request data Add the `--dry-run` flag to see the exact HTTP request that sqlc will make to the the upload endpoint. * docs: Add documentation for the upload subcommand
1 parent 66d4310 commit bde6281

File tree

14 files changed

+347
-16
lines changed

14 files changed

+347
-16
lines changed

docs/guides/privacy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ We provide a few hosted services in addition to the sqlc command line tool.
4949
* Playground data stored in [Google Cloud Storage](https://cloud.google.com/storage)
5050
* Automatically deleted after 30 days
5151

52-
### api.sqlc.dev
52+
### app.sqlc.dev / api.sqlc.dev
5353

5454
* Hosted on [Heroku](https://heroku.com)
5555
* Error tracking and tracing with [Sentry](https://sentry.io)

docs/howto/upload.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Uploading projects
2+
3+
*This feature requires signing up for [sqlc Cloud](https://app.sqlc.dev), which is currently in beta.*
4+
5+
Uploading your project ensures that future releases of sqlc do not break your
6+
existing code. Similar to Rust's [crater](https://github.com/rust-lang/crater)
7+
project, uploaded projects are tested against development releases of sqlc to
8+
verify correctness.
9+
10+
## Add configuration
11+
12+
After creating a project, add the project ID to your sqlc configuration file.
13+
14+
```yaml
15+
version: "1"
16+
project:
17+
id: "<PROJECT-ID>"
18+
packages: []
19+
```
20+
21+
```json
22+
{
23+
"version": "1",
24+
"project": {
25+
"id": "<PROJECT-ID>"
26+
},
27+
"packages": [
28+
]
29+
}
30+
```
31+
32+
You'll also need to create an API token and make it available via the
33+
`SQLC_AUTH_TOKEN` environment variable.
34+
35+
```shell
36+
export SQLC_AUTH_TOKEN=sqlc_xxxxxxxx
37+
```
38+
39+
## Dry run
40+
41+
You can see what's included when uploading your project by using using the `--dry-run` flag:
42+
43+
```shell
44+
sqlc upload --dry-run
45+
```
46+
47+
The output will be the exact HTTP request sent by `sqlc`.
48+
49+
## Upload
50+
51+
Once you're ready to upload, remove the `--dry-run` flag.
52+
53+
```shell
54+
sqlc upload
55+
```
56+
57+
By uploading your project, you're making sqlc more stable and reliable. Thanks!

docs/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ code ever again.
5353
howto/ddl.md
5454
howto/structs.md
5555

56+
howto/upload.md
57+
5658
.. toctree::
5759
:maxdepth: 2
5860
:caption: Reference

docs/reference/cli.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ Usage:
66
77
Available Commands:
88
compile Statically check SQL for syntax and type errors
9+
completion Generate the autocompletion script for the specified shell
910
generate Generate Go code from SQL
1011
help Help about any command
1112
init Create an empty sqlc.yaml settings file
13+
upload Upload the schema, queries, and configuration for this project
1214
version Print the sqlc version number
1315
1416
Flags:
15-
-h, --help help for sqlc
17+
-x, --experimental enable experimental features (default: false)
18+
-f, --file string specify an alternate config file (default: sqlc.yaml)
19+
-h, --help help for sqlc
1620
1721
Use "sqlc [command] --help" for more information about a command.
1822
```

internal/bundler/metadata.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package bundler
2+
3+
import (
4+
"runtime"
5+
6+
"github.com/kyleconroy/sqlc/internal/info"
7+
)
8+
9+
func projectMetadata() ([][2]string, error) {
10+
return [][2]string{
11+
{"sqlc_version", info.Version},
12+
{"go_version", runtime.Version()},
13+
{"goos", runtime.GOOS},
14+
{"goarch", runtime.GOARCH},
15+
}, nil
16+
}

internal/bundler/multipart.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package bundler
2+
3+
import (
4+
"io"
5+
"mime/multipart"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/kyleconroy/sqlc/internal/config"
10+
"github.com/kyleconroy/sqlc/internal/sql/sqlpath"
11+
)
12+
13+
func writeInputs(w *multipart.Writer, file string, conf *config.Config) error {
14+
refs := map[string]struct{}{}
15+
refs[filepath.Base(file)] = struct{}{}
16+
17+
for _, pkg := range conf.SQL {
18+
for _, paths := range []config.Paths{pkg.Schema, pkg.Queries} {
19+
files, err := sqlpath.Glob(paths)
20+
if err != nil {
21+
return err
22+
}
23+
for _, file := range files {
24+
refs[file] = struct{}{}
25+
}
26+
}
27+
}
28+
29+
for file, _ := range refs {
30+
if err := addPart(w, file); err != nil {
31+
return err
32+
}
33+
}
34+
35+
params, err := projectMetadata()
36+
if err != nil {
37+
return err
38+
}
39+
params = append(params, [2]string{"project_id", conf.Project.ID})
40+
for _, val := range params {
41+
if err = w.WriteField(val[0], val[1]); err != nil {
42+
return err
43+
}
44+
}
45+
return nil
46+
}
47+
48+
func addPart(w *multipart.Writer, file string) error {
49+
h, err := os.Open(file)
50+
if err != nil {
51+
return err
52+
}
53+
defer h.Close()
54+
part, err := w.CreateFormFile("inputs", file)
55+
if err != nil {
56+
return err
57+
}
58+
_, err = io.Copy(part, h)
59+
if err != nil {
60+
return err
61+
}
62+
return nil
63+
}
64+
65+
func writeOutputs(w *multipart.Writer, dir string, output map[string]string) error {
66+
for filename, contents := range output {
67+
rel, err := filepath.Rel(dir, filename)
68+
if err != nil {
69+
return err
70+
}
71+
part, err := w.CreateFormFile("outputs", rel)
72+
if err != nil {
73+
return err
74+
}
75+
if _, err := io.WriteString(part, contents); err != nil {
76+
return err
77+
}
78+
}
79+
return nil
80+
}

internal/bundler/upload.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package bundler
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"mime/multipart"
9+
"net/http"
10+
"net/http/httputil"
11+
"os"
12+
13+
"github.com/kyleconroy/sqlc/internal/config"
14+
)
15+
16+
type Uploader struct {
17+
token string
18+
configPath string
19+
config *config.Config
20+
dir string
21+
}
22+
23+
func NewUploader(configPath, dir string, conf *config.Config) *Uploader {
24+
return &Uploader{
25+
token: os.Getenv("SQLC_AUTH_TOKEN"),
26+
configPath: configPath,
27+
config: conf,
28+
dir: dir,
29+
}
30+
}
31+
32+
func (up *Uploader) Validate() error {
33+
if up.config.Project.ID == "" {
34+
return fmt.Errorf("project.id is not set")
35+
}
36+
if up.token == "" {
37+
return fmt.Errorf("SQLC_AUTH_TOKEN environment variable is not set")
38+
}
39+
return nil
40+
}
41+
42+
func (up *Uploader) buildRequest(ctx context.Context, result map[string]string) (*http.Request, error) {
43+
body := bytes.NewBuffer([]byte{})
44+
45+
w := multipart.NewWriter(body)
46+
defer w.Close()
47+
if err := writeInputs(w, up.configPath, up.config); err != nil {
48+
return nil, err
49+
}
50+
if err := writeOutputs(w, up.dir, result); err != nil {
51+
return nil, err
52+
}
53+
w.Close()
54+
55+
req, err := http.NewRequest("POST", "https://api.sqlc.dev/upload", body)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
// Set sqlc-version header
61+
req.Header.Set("Content-Type", w.FormDataContentType())
62+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", up.token))
63+
return req.WithContext(ctx), nil
64+
}
65+
66+
func (up *Uploader) DumpRequestOut(ctx context.Context, result map[string]string) error {
67+
req, err := up.buildRequest(ctx, result)
68+
if err != nil {
69+
return err
70+
}
71+
dump, err := httputil.DumpRequest(req, true)
72+
if err != nil {
73+
return err
74+
}
75+
os.Stdout.Write(dump)
76+
return nil
77+
}
78+
79+
func (up *Uploader) Upload(ctx context.Context, result map[string]string) error {
80+
if err := up.Validate(); err != nil {
81+
return err
82+
}
83+
req, err := up.buildRequest(ctx, result)
84+
if err != nil {
85+
return err
86+
}
87+
client := &http.Client{}
88+
resp, err := client.Do(req)
89+
if err != nil {
90+
return err
91+
}
92+
if resp.StatusCode >= 400 {
93+
body, err := io.ReadAll(resp.Body)
94+
defer resp.Body.Close()
95+
if err != nil {
96+
return fmt.Errorf("upload error: endpoint returned non-200 status code: %d", resp.StatusCode)
97+
}
98+
return fmt.Errorf("upload error: %d: %s", resp.StatusCode, string(body))
99+
}
100+
return nil
101+
}

internal/cmd/cmd.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/kyleconroy/sqlc/internal/config"
1717
"github.com/kyleconroy/sqlc/internal/debug"
18+
"github.com/kyleconroy/sqlc/internal/info"
1819
"github.com/kyleconroy/sqlc/internal/tracer"
1920
)
2021

@@ -28,6 +29,8 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
2829
rootCmd.AddCommand(genCmd)
2930
rootCmd.AddCommand(initCmd)
3031
rootCmd.AddCommand(versionCmd)
32+
uploadCmd.Flags().BoolP("dry-run", "", false, "dump upload request (default: false)")
33+
rootCmd.AddCommand(uploadCmd)
3134

3235
rootCmd.SetArgs(args)
3336
rootCmd.SetIn(stdin)
@@ -60,9 +63,7 @@ var versionCmd = &cobra.Command{
6063
defer trace.StartRegion(cmd.Context(), "version").End()
6164
}
6265
if version == "" {
63-
// When no version is set, return the next bug fix version
64-
// after the most recent tag
65-
fmt.Printf("%s\n", "v1.12.0")
66+
fmt.Printf("%s\n", info.Version)
6667
} else {
6768
fmt.Printf("%s\n", version)
6869
}
@@ -96,11 +97,16 @@ var initCmd = &cobra.Command{
9697

9798
type Env struct {
9899
ExperimentalFeatures bool
100+
DryRun bool
99101
}
100102

101103
func ParseEnv(c *cobra.Command) Env {
102104
x := c.Flag("experimental")
103-
return Env{ExperimentalFeatures: x != nil && x.Changed}
105+
dr := c.Flag("dry-run")
106+
return Env{
107+
ExperimentalFeatures: x != nil && x.Changed,
108+
DryRun: dr != nil && dr.Changed,
109+
}
104110
}
105111

106112
func getConfigPath(stderr io.Writer, f *pflag.Flag) (string, string) {
@@ -152,6 +158,20 @@ var genCmd = &cobra.Command{
152158
},
153159
}
154160

161+
var uploadCmd = &cobra.Command{
162+
Use: "upload",
163+
Short: "Upload the schema, queries, and configuration for this project",
164+
RunE: func(cmd *cobra.Command, args []string) error {
165+
stderr := cmd.ErrOrStderr()
166+
dir, name := getConfigPath(stderr, cmd.Flag("file"))
167+
if err := createPkg(cmd.Context(), ParseEnv(cmd), dir, name, stderr); err != nil {
168+
fmt.Fprintf(stderr, "error uploading: %s\n", err)
169+
os.Exit(1)
170+
}
171+
return nil
172+
},
173+
}
174+
155175
var checkCmd = &cobra.Command{
156176
Use: "compile",
157177
Short: "Statically check SQL for syntax and type errors",

0 commit comments

Comments
 (0)