Repository: kyleconroy/sqlc
Pull request #1495 (2022-03-18 01:25:23)

Added postgres CALL support
Not sure if I've gone about this the right way but it seems to generate ok.

Let me know if there's anything else that needs to be done for this.


Patch:
diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
new file mode 100644
index 000000000..8a78992c4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
@@ -0,0 +1,29 @@
+name: Feature Request
+description: Request a new fetaure or a change to an existing feature
+labels: [enhancement, triage]
+body:
+  - type: textarea
+    id: feature
+    attributes:
+      label: What do you want to change?
+      placeholder: Tell us what you want
+      value: "Free unicorns!"
+    validations:
+      required: true
+  - type: dropdown
+    id: engines
+    attributes:
+      label: What database engines need to be changed?
+      multiple: true
+      options:
+        - PostgreSQL
+        - MySQL
+  - type: dropdown
+    id: languages
+    attributes:
+      label: What programming language backends need to be changed?
+      multiple: true
+      options:
+        - Go
+        - Python
+        - Kotlin
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c320b68f0..b37de1073 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,7 +34,7 @@ jobs:
 
     - uses: actions/setup-go@v2
       with:
-        go-version: '1.17'
+        go-version: '1.18'
 
     - name: test ./...
       run: go test --tags=examples ./...
diff --git a/docs/guides/privacy.md b/docs/guides/privacy.md
index 1422fea99..da29fd885 100644
--- a/docs/guides/privacy.md
+++ b/docs/guides/privacy.md
@@ -49,7 +49,7 @@ We provide a few hosted services in addition to the sqlc command line tool.
 * Playground data stored in [Google Cloud Storage](https://cloud.google.com/storage)
   * Automatically deleted after 30 days
 
-### api.sqlc.dev
+### app.sqlc.dev / api.sqlc.dev
 
 * Hosted on [Heroku](https://heroku.com)
 * Error tracking and tracing with [Sentry](https://sentry.io)
diff --git a/docs/howto/update.md b/docs/howto/update.md
index 2aeaf4c8c..9450f3d99 100644
--- a/docs/howto/update.md
+++ b/docs/howto/update.md
@@ -5,12 +5,53 @@ CREATE TABLE authors (
   id         SERIAL PRIMARY KEY,
   bio        text   NOT NULL
 );
+```
 
--- name: UpdateAuthor :exec
-UPDATE authors SET bio = $2
-WHERE id = $1;
+## Single parameter
+
+If your query has a single parameter, your Go method will also have a single
+parameter.
+
+```sql
+-- name: UpdateAuthorBios :exec
+UPDATE authors SET bio = $1;
+```
+
+```go
+package db
+
+import (
+	"context"
+	"database/sql"
+)
+
+type DBTX interface {
+	ExecContext(context.Context, string, ...interface{}) error
+}
+
+func New(db DBTX) *Queries {
+	return &Queries{db: db}
+}
+
+type Queries struct {
+	db DBTX
+}
+
+const updateAuthorBios = `-- name: UpdateAuthorBios :exec
+UPDATE authors SET bio = $1
+`
+
+func (q *Queries) UpdateAuthorBios(ctx context.Context, bio string) error {
+	_, err := q.db.ExecContext(ctx, updateAuthorBios, bio)
+	return err
+}
 ```
 
+## Multiple parameters
+
+If your query has more than one parameter, your Go method will accept a
+`Params` struct.
+
 ```go
 package db
 
@@ -36,8 +77,14 @@ UPDATE authors SET bio = $2
 WHERE id = $1
 `
 
-func (q *Queries) UpdateAuthor(ctx context.Context, id int, bio string) error {
-	_, err := q.db.ExecContext(ctx, updateAuthor, id, bio)
+type UpdateAuthorParams struct {
+	ID  int32
+	Bio string
+}
+
+func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) error {
+	_, err := q.db.ExecContext(ctx, updateAuthor, arg.ID, arg.Bio)
 	return err
 }
 ```
+
diff --git a/docs/howto/upload.md b/docs/howto/upload.md
new file mode 100644
index 000000000..0add35c94
--- /dev/null
+++ b/docs/howto/upload.md
@@ -0,0 +1,57 @@
+# Uploading projects
+
+*This feature requires signing up for [sqlc Cloud](https://app.sqlc.dev), which is currently in beta.*
+
+Uploading your project ensures that future releases of sqlc do not break your
+existing code. Similar to Rust's [crater](https://github.com/rust-lang/crater)
+project, uploaded projects are tested against development releases of sqlc to
+verify correctness.
+
+## Add configuration
+
+After creating a project, add the project ID to your sqlc configuration file.
+
+```yaml
+version: "1"
+project:
+  id: "<PROJECT-ID>"
+packages: []
+```
+
+```json
+{
+  "version": "1",
+  "project": {
+    "id": "<PROJECT-ID>"
+  },
+  "packages": [
+  ]
+}
+```
+
+You'll also need to create an API token and make it available via the
+`SQLC_AUTH_TOKEN` environment variable.
+
+```shell
+export SQLC_AUTH_TOKEN=sqlc_xxxxxxxx
+```
+
+## Dry run
+
+You can see what's included when uploading your project by using using the `--dry-run` flag:
+
+```shell
+sqlc upload --dry-run
+```
+
+The output will be the exact HTTP request sent by `sqlc`.
+
+## Upload
+
+Once you're ready to upload, remove the `--dry-run` flag.
+
+```shell
+sqlc upload
+```
+
+By uploading your project, you're making sqlc more stable and reliable. Thanks!
diff --git a/docs/index.rst b/docs/index.rst
index e0f39532f..91b68c02f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -53,6 +53,8 @@ code ever again.
    howto/ddl.md
    howto/structs.md
 
+   howto/upload.md
+
 .. toctree::
    :maxdepth: 2
    :caption: Reference
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index ec55f3b9e..a6033f220 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -6,13 +6,17 @@ Usage:
 
 Available Commands:
   compile     Statically check SQL for syntax and type errors
+  completion  Generate the autocompletion script for the specified shell
   generate    Generate Go code from SQL
   help        Help about any command
   init        Create an empty sqlc.yaml settings file
+  upload      Upload the schema, queries, and configuration for this project
   version     Print the sqlc version number
 
 Flags:
-  -h, --help   help for sqlc
+  -x, --experimental   enable experimental features (default: false)
+  -f, --file string    specify an alternate config file (default: sqlc.yaml)
+  -h, --help           help for sqlc
 
 Use "sqlc [command] --help" for more information about a command.
 ```
diff --git a/go.mod b/go.mod
index 937c6a1e6..cab928c72 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@ require (
 	github.com/pingcap/parser v0.0.0-20210914110036-002913dd28ec
 	github.com/spf13/cobra v1.4.0
 	github.com/spf13/pflag v1.0.5
-	google.golang.org/protobuf v1.27.1
+	google.golang.org/protobuf v1.28.0
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 )
 
diff --git a/go.sum b/go.sum
index b0ef5828a..30b7b64cc 100644
--- a/go.sum
+++ b/go.sum
@@ -262,8 +262,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/bundler/metadata.go b/internal/bundler/metadata.go
new file mode 100644
index 000000000..415b131ef
--- /dev/null
+++ b/internal/bundler/metadata.go
@@ -0,0 +1,16 @@
+package bundler
+
+import (
+	"runtime"
+
+	"github.com/kyleconroy/sqlc/internal/info"
+)
+
+func projectMetadata() ([][2]string, error) {
+	return [][2]string{
+		{"sqlc_version", info.Version},
+		{"go_version", runtime.Version()},
+		{"goos", runtime.GOOS},
+		{"goarch", runtime.GOARCH},
+	}, nil
+}
diff --git a/internal/bundler/multipart.go b/internal/bundler/multipart.go
new file mode 100644
index 000000000..a755bbfcf
--- /dev/null
+++ b/internal/bundler/multipart.go
@@ -0,0 +1,80 @@
+package bundler
+
+import (
+	"io"
+	"mime/multipart"
+	"os"
+	"path/filepath"
+
+	"github.com/kyleconroy/sqlc/internal/config"
+	"github.com/kyleconroy/sqlc/internal/sql/sqlpath"
+)
+
+func writeInputs(w *multipart.Writer, file string, conf *config.Config) error {
+	refs := map[string]struct{}{}
+	refs[filepath.Base(file)] = struct{}{}
+
+	for _, pkg := range conf.SQL {
+		for _, paths := range []config.Paths{pkg.Schema, pkg.Queries} {
+			files, err := sqlpath.Glob(paths)
+			if err != nil {
+				return err
+			}
+			for _, file := range files {
+				refs[file] = struct{}{}
+			}
+		}
+	}
+
+	for file, _ := range refs {
+		if err := addPart(w, file); err != nil {
+			return err
+		}
+	}
+
+	params, err := projectMetadata()
+	if err != nil {
+		return err
+	}
+	params = append(params, [2]string{"project_id", conf.Project.ID})
+	for _, val := range params {
+		if err = w.WriteField(val[0], val[1]); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func addPart(w *multipart.Writer, file string) error {
+	h, err := os.Open(file)
+	if err != nil {
+		return err
+	}
+	defer h.Close()
+	part, err := w.CreateFormFile("inputs", file)
+	if err != nil {
+		return err
+	}
+	_, err = io.Copy(part, h)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func writeOutputs(w *multipart.Writer, dir string, output map[string]string) error {
+	for filename, contents := range output {
+		rel, err := filepath.Rel(dir, filename)
+		if err != nil {
+			return err
+		}
+		part, err := w.CreateFormFile("outputs", rel)
+		if err != nil {
+			return err
+		}
+		if _, err := io.WriteString(part, contents); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/internal/bundler/upload.go b/internal/bundler/upload.go
new file mode 100644
index 000000000..8f09f8562
--- /dev/null
+++ b/internal/bundler/upload.go
@@ -0,0 +1,101 @@
+package bundler
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"net/http/httputil"
+	"os"
+
+	"github.com/kyleconroy/sqlc/internal/config"
+)
+
+type Uploader struct {
+	token      string
+	configPath string
+	config     *config.Config
+	dir        string
+}
+
+func NewUploader(configPath, dir string, conf *config.Config) *Uploader {
+	return &Uploader{
+		token:      os.Getenv("SQLC_AUTH_TOKEN"),
+		configPath: configPath,
+		config:     conf,
+		dir:        dir,
+	}
+}
+
+func (up *Uploader) Validate() error {
+	if up.config.Project.ID == "" {
+		return fmt.Errorf("project.id is not set")
+	}
+	if up.token == "" {
+		return fmt.Errorf("SQLC_AUTH_TOKEN environment variable is not set")
+	}
+	return nil
+}
+
+func (up *Uploader) buildRequest(ctx context.Context, result map[string]string) (*http.Request, error) {
+	body := bytes.NewBuffer([]byte{})
+
+	w := multipart.NewWriter(body)
+	defer w.Close()
+	if err := writeInputs(w, up.configPath, up.config); err != nil {
+		return nil, err
+	}
+	if err := writeOutputs(w, up.dir, result); err != nil {
+		return nil, err
+	}
+	w.Close()
+
+	req, err := http.NewRequest("POST", "https://api.sqlc.dev/upload", body)
+	if err != nil {
+		return nil, err
+	}
+
+	// Set sqlc-version header
+	req.Header.Set("Content-Type", w.FormDataContentType())
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", up.token))
+	return req.WithContext(ctx), nil
+}
+
+func (up *Uploader) DumpRequestOut(ctx context.Context, result map[string]string) error {
+	req, err := up.buildRequest(ctx, result)
+	if err != nil {
+		return err
+	}
+	dump, err := httputil.DumpRequest(req, true)
+	if err != nil {
+		return err
+	}
+	os.Stdout.Write(dump)
+	return nil
+}
+
+func (up *Uploader) Upload(ctx context.Context, result map[string]string) error {
+	if err := up.Validate(); err != nil {
+		return err
+	}
+	req, err := up.buildRequest(ctx, result)
+	if err != nil {
+		return err
+	}
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode >= 400 {
+		body, err := io.ReadAll(resp.Body)
+		defer resp.Body.Close()
+		if err != nil {
+			return fmt.Errorf("upload error: endpoint returned non-200 status code: %d", resp.StatusCode)
+		}
+		return fmt.Errorf("upload error: %d: %s", resp.StatusCode, string(body))
+	}
+	return nil
+}
diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go
index 5cefe6a64..4c36e96b3 100644
--- a/internal/cmd/cmd.go
+++ b/internal/cmd/cmd.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/kyleconroy/sqlc/internal/config"
 	"github.com/kyleconroy/sqlc/internal/debug"
+	"github.com/kyleconroy/sqlc/internal/info"
 	"github.com/kyleconroy/sqlc/internal/tracer"
 )
 
@@ -28,6 +29,8 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
 	rootCmd.AddCommand(genCmd)
 	rootCmd.AddCommand(initCmd)
 	rootCmd.AddCommand(versionCmd)
+	uploadCmd.Flags().BoolP("dry-run", "", false, "dump upload request (default: false)")
+	rootCmd.AddCommand(uploadCmd)
 
 	rootCmd.SetArgs(args)
 	rootCmd.SetIn(stdin)
@@ -60,9 +63,7 @@ var versionCmd = &cobra.Command{
 			defer trace.StartRegion(cmd.Context(), "version").End()
 		}
 		if version == "" {
-			// When no version is set, return the next bug fix version
-			// after the most recent tag
-			fmt.Printf("%s\n", "v1.12.0")
+			fmt.Printf("%s\n", info.Version)
 		} else {
 			fmt.Printf("%s\n", version)
 		}
@@ -96,11 +97,16 @@ var initCmd = &cobra.Command{
 
 type Env struct {
 	ExperimentalFeatures bool
+	DryRun               bool
 }
 
 func ParseEnv(c *cobra.Command) Env {
 	x := c.Flag("experimental")
-	return Env{ExperimentalFeatures: x != nil && x.Changed}
+	dr := c.Flag("dry-run")
+	return Env{
+		ExperimentalFeatures: x != nil && x.Changed,
+		DryRun:               dr != nil && dr.Changed,
+	}
 }
 
 func getConfigPath(stderr io.Writer, f *pflag.Flag) (string, string) {
@@ -152,6 +158,20 @@ var genCmd = &cobra.Command{
 	},
 }
 
+var uploadCmd = &cobra.Command{
+	Use:   "upload",
+	Short: "Upload the schema, queries, and configuration for this project",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		stderr := cmd.ErrOrStderr()
+		dir, name := getConfigPath(stderr, cmd.Flag("file"))
+		if err := createPkg(cmd.Context(), ParseEnv(cmd), dir, name, stderr); err != nil {
+			fmt.Fprintf(stderr, "error uploading: %s\n", err)
+			os.Exit(1)
+		}
+		return nil
+	},
+}
+
 var checkCmd = &cobra.Command{
 	Use:   "compile",
 	Short: "Statically check SQL for syntax and type errors",
diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go
index c25d37bc0..9d53cd4f9 100644
--- a/internal/cmd/generate.go
+++ b/internal/cmd/generate.go
@@ -47,7 +47,7 @@ type outPair struct {
 	config.SQL
 }
 
-func Generate(ctx context.Context, e Env, dir, filename string, stderr io.Writer) (map[string]string, error) {
+func readConfig(stderr io.Writer, dir, filename string) (string, *config.Config, error) {
 	configPath := ""
 	if filename != "" {
 		configPath = filepath.Join(dir, filename)
@@ -65,12 +65,12 @@ func Generate(ctx context.Context, e Env, dir, filename string, stderr io.Writer
 
 		if yamlMissing && jsonMissing {
 			fmt.Fprintln(stderr, "error parsing configuration files. sqlc.yaml or sqlc.json: file does not exist")
-			return nil, errors.New("config file missing")
+			return "", nil, errors.New("config file missing")
 		}
 
 		if !yamlMissing && !jsonMissing {
 			fmt.Fprintln(stderr, "error: both sqlc.json and sqlc.yaml files present")
-			return nil, errors.New("sqlc.json and sqlc.yaml present")
+			return "", nil, errors.New("sqlc.json and sqlc.yaml present")
 		}
 
 		configPath = yamlPath
@@ -83,7 +83,7 @@ func Generate(ctx context.Context, e Env, dir, filename string, stderr io.Writer
 	blob, err := os.ReadFile(configPath)
 	if err != nil {
 		fmt.Fprintf(stderr, "error parsing %s: file does not exist\n", base)
-		return nil, err
+		return "", nil, err
 	}
 
 	conf, err := config.ParseConfig(bytes.NewReader(blob))
@@ -97,9 +97,19 @@ func Generate(ctx context.Context, e Env, dir, filename string, stderr io.Writer
 			fmt.Fprintf(stderr, errMessageNoPackages)
 		}
 		fmt.Fprintf(stderr, "error parsing %s: %s\n", base, err)
+		return "", nil, err
+	}
+
+	return configPath, &conf, nil
+}
+
+func Generate(ctx context.Context, e Env, dir, filename string, stderr io.Writer) (map[string]string, error) {
+	configPath, conf, err := readConfig(stderr, dir, filename)
+	if err != nil {
 		return nil, err
 	}
 
+	base := filepath.Base(configPath)
 	if err := config.Validate(conf); err != nil {
 		fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
 		return nil, err
@@ -135,7 +145,7 @@ func Generate(ctx context.Context, e Env, dir, filename string, stderr io.Writer
 	}
 
 	for _, sql := range pairs {
-		combo := config.Combine(conf, sql.SQL)
+		combo := config.Combine(*conf, sql.SQL)
 
 		// TODO: This feels like a hack that will bite us later
 		joined := make([]string, 0, len(sql.Schema))
diff --git a/internal/cmd/package.go b/internal/cmd/package.go
new file mode 100644
index 000000000..63a4e0050
--- /dev/null
+++ b/internal/cmd/package.go
@@ -0,0 +1,29 @@
+package cmd
+
+import (
+	"context"
+	"io"
+	"os"
+
+	"github.com/kyleconroy/sqlc/internal/bundler"
+)
+
+func createPkg(ctx context.Context, e Env, dir, filename string, stderr io.Writer) error {
+	configPath, conf, err := readConfig(stderr, dir, filename)
+	if err != nil {
+		return err
+	}
+	up := bundler.NewUploader(configPath, dir, conf)
+	if err := up.Validate(); err != nil {
+		return err
+	}
+	output, err := Generate(ctx, e, dir, filename, stderr)
+	if err != nil {
+		os.Exit(1)
+	}
+	if e.DryRun {
+		return up.DumpRequestOut(ctx, output)
+	} else {
+		return up.Upload(ctx, output)
+	}
+}
diff --git a/internal/compiler/find_params.go b/internal/compiler/find_params.go
index 8c7a325c3..62eb2fb02 100644
--- a/internal/compiler/find_params.go
+++ b/internal/compiler/find_params.go
@@ -66,6 +66,9 @@ func (p paramSearch) Visit(node ast.Node) astutils.Visitor {
 	case *ast.BetweenExpr:
 		p.parent = node
 
+	case *ast.CallStmt:
+		p.parent = n.FuncCall
+
 	case *ast.FuncCall:
 		p.parent = node
 
diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go
index 65a2d5853..a01675645 100644
--- a/internal/compiler/output_columns.go
+++ b/internal/compiler/output_columns.go
@@ -83,6 +83,8 @@ func outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, error) {
 		if len(targets.Items) == 0 && n.Larg != nil {
 			return outputColumns(qc, n.Larg)
 		}
+	case *ast.CallStmt:
+		targets = &ast.List{}
 	case *ast.TruncateStmt:
 		targets = &ast.List{}
 	case *ast.UpdateStmt:
@@ -386,6 +388,8 @@ func sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) {
 		list = &ast.List{
 			Items: append(n.FromClause.Items, n.Relations.Items...),
 		}
+	case *ast.CallStmt:
+		list = &ast.List{}
 	default:
 		return nil, fmt.Errorf("sourceTables: unsupported node type: %T", n)
 	}
diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go
index 9beaead85..017a32679 100644
--- a/internal/compiler/parse.go
+++ b/internal/compiler/parse.go
@@ -48,6 +48,7 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
 	}
 	var table *ast.TableName
 	switch n := raw.Stmt.(type) {
+	case *ast.CallStmt:
 	case *ast.SelectStmt:
 	case *ast.DeleteStmt:
 	case *ast.InsertStmt:
diff --git a/internal/config/config.go b/internal/config/config.go
index 66ba3f261..0cc641925 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -78,9 +78,14 @@ const (
 )
 
 type Config struct {
-	Version string `json:"version" yaml:"version"`
-	SQL     []SQL  `json:"sql" yaml:"sql"`
-	Gen     Gen    `json:"overrides,omitempty" yaml:"overrides"`
+	Version string  `json:"version" yaml:"version"`
+	Project Project `json:"project" yaml:"project"`
+	SQL     []SQL   `json:"sql" yaml:"sql"`
+	Gen     Gen     `json:"overrides,omitempty" yaml:"overrides"`
+}
+
+type Project struct {
+	ID string `json:"id" yaml:"id"`
 }
 
 type Gen struct {
@@ -327,7 +332,7 @@ func ParseConfig(rd io.Reader) (Config, error) {
 	}
 }
 
-func Validate(c Config) error {
+func Validate(c *Config) error {
 	for _, sql := range c.SQL {
 		sqlGo := sql.Gen.Go
 		if sqlGo == nil {
diff --git a/internal/config/v_one.go b/internal/config/v_one.go
index d1ad4fc11..5b357255b 100644
--- a/internal/config/v_one.go
+++ b/internal/config/v_one.go
@@ -10,6 +10,7 @@ import (
 
 type V1GenerateSettings struct {
 	Version   string              `json:"version" yaml:"version"`
+	Project   Project             `json:"project" yaml:"project"`
 	Packages  []v1PackageSettings `json:"packages" yaml:"packages"`
 	Overrides []Override          `json:"overrides,omitempty" yaml:"overrides,omitempty"`
 	Rename    map[string]string   `json:"rename,omitempty" yaml:"rename,omitempty"`
@@ -105,6 +106,7 @@ func (c *V1GenerateSettings) ValidateGlobalOverrides() error {
 func (c *V1GenerateSettings) Translate() Config {
 	conf := Config{
 		Version: c.Version,
+		Project: c.Project,
 	}
 
 	for _, pkg := range c.Packages {
diff --git a/internal/engine/postgresql/convert.go b/internal/engine/postgresql/convert.go
index c6e8976b6..ce420a502 100644
--- a/internal/engine/postgresql/convert.go
+++ b/internal/engine/postgresql/convert.go
@@ -639,6 +639,33 @@ func convertBooleanTest(n *pg.BooleanTest) *ast.BooleanTest {
 	}
 }
 
+func convertCallStmt(n *pg.CallStmt) *ast.CallStmt {
+	if n == nil {
+		return nil
+	}
+	rel, err := parseRelationFromNodes(n.Funccall.Funcname)
+	if err != nil {
+		// TODO: How should we handle errors?
+		panic(err)
+	}
+
+	return &ast.CallStmt{
+		FuncCall: &ast.FuncCall{
+			Func:           rel.FuncName(),
+			Funcname:       convertSlice(n.Funccall.Funcname),
+			Args:           convertSlice(n.Funccall.Args),
+			AggOrder:       convertSlice(n.Funccall.AggOrder),
+			AggFilter:      convertNode(n.Funccall.AggFilter),
+			AggWithinGroup: n.Funccall.AggWithinGroup,
+			AggStar:        n.Funccall.AggStar,
+			AggDistinct:    n.Funccall.AggDistinct,
+			FuncVariadic:   n.Funccall.FuncVariadic,
+			Over:           convertWindowDef(n.Funccall.Over),
+			Location:       int(n.Funccall.Location),
+		},
+	}
+}
+
 func convertCaseExpr(n *pg.CaseExpr) *ast.CaseExpr {
 	if n == nil {
 		return nil
@@ -3092,6 +3119,9 @@ func convertNode(node *pg.Node) ast.Node {
 
 	case *pg.Node_BooleanTest:
 		return convertBooleanTest(n.BooleanTest)
+		
+	case *pg.Node_CallStmt:
+		return convertCallStmt(n.CallStmt)
 
 	case *pg.Node_CaseExpr:
 		return convertCaseExpr(n.CaseExpr)
diff --git a/internal/info/facts.go b/internal/info/facts.go
new file mode 100644
index 000000000..3c4b47849
--- /dev/null
+++ b/internal/info/facts.go
@@ -0,0 +1,5 @@
+package info
+
+// When no version is set, return the next bug fix version
+// after the most recent tag
+const Version = "v1.12.0"
diff --git a/internal/sql/ast/call_stmt.go b/internal/sql/ast/call_stmt.go
new file mode 100644
index 000000000..252bfb316
--- /dev/null
+++ b/internal/sql/ast/call_stmt.go
@@ -0,0 +1,12 @@
+package ast
+
+type CallStmt struct {
+	FuncCall *FuncCall
+}
+
+func (n *CallStmt) Pos() int {
+	if n.FuncCall == nil {
+		return 0
+	}
+	return n.FuncCall.Pos()
+}
diff --git a/internal/sql/astutils/rewrite.go b/internal/sql/astutils/rewrite.go
index 209cfb382..2735d6e3e 100644
--- a/internal/sql/astutils/rewrite.go
+++ b/internal/sql/astutils/rewrite.go
@@ -414,6 +414,9 @@ func (a *application) apply(parent ast.Node, name string, iter *iterator, n ast.
 		a.apply(n, "Xpr", nil, n.Xpr)
 		a.apply(n, "Arg", nil, n.Arg)
 
+	case *ast.CallStmt:
+		a.apply(n, "FuncCall", nil, n.FuncCall)
+
 	case *ast.CaseExpr:
 		a.apply(n, "Xpr", nil, n.Xpr)
 		a.apply(n, "Arg", nil, n.Arg)
diff --git a/internal/sql/astutils/walk.go b/internal/sql/astutils/walk.go
index eefad0ac0..d1b4ee6aa 100644
--- a/internal/sql/astutils/walk.go
+++ b/internal/sql/astutils/walk.go
@@ -511,6 +511,11 @@ func Walk(f Visitor, node ast.Node) {
 			Walk(f, n.Arg)
 		}
 
+	case *ast.CallStmt:
+		if n.FuncCall != nil {
+			Walk(f, n.FuncCall)
+		}
+
 	case *ast.CaseExpr:
 		if n.Xpr != nil {
 			Walk(f, n.Xpr)


Test patch:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 9250629c2..be84dc82f 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -64,8 +64,20 @@ func TestBadConfigs(t *testing.T) {
 	}
 }
 
+const validConfigOne = `{
+  "version": "1"
+  "packages": []
+}`
+
+func FuzzConfig(f *testing.F) {
+	f.Add(validConfigOne)
+	f.Fuzz(func(t *testing.T, orig string) {
+		ParseConfig(strings.NewReader(orig))
+	})
+}
+
 func TestInvalidConfig(t *testing.T) {
-	err := Validate(Config{
+	err := Validate(&Config{
 		SQL: []SQL{{
 			Gen: SQLGen{
 				Go: &SQLGo{
@@ -163,3 +175,19 @@ func TestTypeOverrides(t *testing.T) {
 		})
 	}
 }
+
+func FuzzOverride(f *testing.F) {
+	for _, spec := range []string{
+		"string",
+		"github.com/gofrs/uuid.UUID",
+		"github.com/segmentio/ksuid.KSUID",
+	} {
+		f.Add(spec)
+	}
+	f.Fuzz(func(t *testing.T, s string) {
+		o := Override{
+			GoType: GoType{Spec: s},
+		}
+		o.Parse()
+	})
+}
diff --git a/internal/endtoend/testdata/ddl_create_procedure/postgresql/pgx/go/query.sql.go b/internal/endtoend/testdata/ddl_create_procedure/postgresql/pgx/go/query.sql.go
index c40d6f256..7aee5bd63 100644
--- a/internal/endtoend/testdata/ddl_create_procedure/postgresql/pgx/go/query.sql.go
+++ b/internal/endtoend/testdata/ddl_create_procedure/postgresql/pgx/go/query.sql.go
@@ -7,11 +7,53 @@ import (
 	"context"
 )
 
-const placeholder = `-- name: Placeholder :exec
-SELECT 1
+const callInsertData = `-- name: CallInsertData :exec
+CALL insert_data($1, $2)
 `
 
-func (q *Queries) Placeholder(ctx context.Context) error {
-	_, err := q.db.Exec(ctx, placeholder)
+type CallInsertDataParams struct {
+	A int32
+	B int32
+}
+
+func (q *Queries) CallInsertData(ctx context.Context, arg CallInsertDataParams) error {
+	_, err := q.db.Exec(ctx, callInsertData, arg.A, arg.B)
+	return err
+}
+
+const callInsertDataNamed = `-- name: CallInsertDataNamed :exec
+CALL insert_data(b => $1, a => $2)
+`
+
+type CallInsertDataNamedParams struct {
+	B int32
+	A int32
+}
+
+func (q *Queries) CallInsertDataNamed(ctx context.Context, arg CallInsertDataNamedParams) error {
+	_, err := q.db.Exec(ctx, callInsertDataNamed, arg.B, arg.A)
+	return err
+}
+
+const callInsertDataNoArgs = `-- name: CallInsertDataNoArgs :exec
+CALL insert_data(1, 2)
+`
+
+func (q *Queries) CallInsertDataNoArgs(ctx context.Context) error {
+	_, err := q.db.Exec(ctx, callInsertDataNoArgs)
+	return err
+}
+
+const callInsertDataSqlcArgs = `-- name: CallInsertDataSqlcArgs :exec
+CALL insert_data($1, $2)
+`
+
+type CallInsertDataSqlcArgsParams struct {
+	Foo int32
+	Bar int32
+}
+
+func (q *Queries) CallInsertDataSqlcArgs(ctx context.Context, arg CallInsertDataSqlcArgsParams) error {
+	_, err := q.db.Exec(ctx, callInsertDataSqlcArgs, arg.Foo, arg.Bar)
 	return err
 }
diff --git a/internal/endtoend/testdata/ddl_create_procedure/postgresql/pgx/query.sql b/internal/endtoend/testdata/ddl_create_procedure/postgresql/pgx/query.sql
index eea032f74..2d8d86ad0 100644
--- a/internal/endtoend/testdata/ddl_create_procedure/postgresql/pgx/query.sql
+++ b/internal/endtoend/testdata/ddl_create_procedure/postgresql/pgx/query.sql
@@ -1,6 +1,11 @@
--- name: Placeholder :exec
-SELECT 1;
+-- name: CallInsertData :exec
+CALL insert_data($1, $2);
 
--- FIXME: Implement CALL
--- name: CallInsertData :select
+-- name: CallInsertDataNoArgs :exec
 CALL insert_data(1, 2);
+
+-- name: CallInsertDataNamed :exec
+CALL insert_data(b => $1, a => $2);
+
+-- name: CallInsertDataSqlcArgs :exec
+CALL insert_data(sqlc.arg('foo'), sqlc.arg('bar'));
diff --git a/internal/endtoend/testdata/ddl_create_procedure/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/ddl_create_procedure/postgresql/stdlib/go/query.sql.go
index 203f0fe62..ca07b6700 100644
--- a/internal/endtoend/testdata/ddl_create_procedure/postgresql/stdlib/go/query.sql.go
+++ b/internal/endtoend/testdata/ddl_create_procedure/postgresql/stdlib/go/query.sql.go
@@ -7,11 +7,53 @@ import (
 	"context"
 )
 
-const placeholder = `-- name: Placeholder :exec
-SELECT 1
+const callInsertData = `-- name: CallInsertData :exec
+CALL insert_data($1, $2)
 `
 
-func (q *Queries) Placeholder(ctx context.Context) error {
-	_, err := q.db.ExecContext(ctx, placeholder)
+type CallInsertDataParams struct {
+	A int32
+	B int32
+}
+
+func (q *Queries) CallInsertData(ctx context.Context, arg CallInsertDataParams) error {
+	_, err := q.db.ExecContext(ctx, callInsertData, arg.A, arg.B)
+	return err
+}
+
+const callInsertDataNamed = `-- name: CallInsertDataNamed :exec
+CALL insert_data(b => $1, a => $2)
+`
+
+type CallInsertDataNamedParams struct {
+	B int32
+	A int32
+}
+
+func (q *Queries) CallInsertDataNamed(ctx context.Context, arg CallInsertDataNamedParams) error {
+	_, err := q.db.ExecContext(ctx, callInsertDataNamed, arg.B, arg.A)
+	return err
+}
+
+const callInsertDataNoArgs = `-- name: CallInsertDataNoArgs :exec
+CALL insert_data(1, 2)
+`
+
+func (q *Queries) CallInsertDataNoArgs(ctx context.Context) error {
+	_, err := q.db.ExecContext(ctx, callInsertDataNoArgs)
+	return err
+}
+
+const callInsertDataSqlcArgs = `-- name: CallInsertDataSqlcArgs :exec
+CALL insert_data($1, $2)
+`
+
+type CallInsertDataSqlcArgsParams struct {
+	Foo int32
+	Bar int32
+}
+
+func (q *Queries) CallInsertDataSqlcArgs(ctx context.Context, arg CallInsertDataSqlcArgsParams) error {
+	_, err := q.db.ExecContext(ctx, callInsertDataSqlcArgs, arg.Foo, arg.Bar)
 	return err
 }
diff --git a/internal/endtoend/testdata/ddl_create_procedure/postgresql/stdlib/query.sql b/internal/endtoend/testdata/ddl_create_procedure/postgresql/stdlib/query.sql
index eea032f74..2d8d86ad0 100644
--- a/internal/endtoend/testdata/ddl_create_procedure/postgresql/stdlib/query.sql
+++ b/internal/endtoend/testdata/ddl_create_procedure/postgresql/stdlib/query.sql
@@ -1,6 +1,11 @@
--- name: Placeholder :exec
-SELECT 1;
+-- name: CallInsertData :exec
+CALL insert_data($1, $2);
 
--- FIXME: Implement CALL
--- name: CallInsertData :select
+-- name: CallInsertDataNoArgs :exec
 CALL insert_data(1, 2);
+
+-- name: CallInsertDataNamed :exec
+CALL insert_data(b => $1, a => $2);
+
+-- name: CallInsertDataSqlcArgs :exec
+CALL insert_data(sqlc.arg('foo'), sqlc.arg('bar'));