From 6440eb064d8499a4e078161bca3ec7e2ddb47236 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 29 Jun 2023 15:55:30 -0700 Subject: [PATCH 1/3] feat(cmd/vet): Add built-in db-prepare rule Instead of always preparing a query when running vet, opt-in to doing it with the `db-prepare` rule. --- examples/authors/sqlc.json | 9 +++++++++ examples/batch/sqlc.json | 3 +++ examples/booktest/sqlc.json | 15 +++++++++++--- examples/jets/sqlc.json | 5 ++++- examples/ondeck/sqlc.json | 9 +++++++++ internal/cmd/vet.go | 39 ++++++++++++++++++++++++++++--------- internal/config/v_one.go | 4 ++++ 7 files changed, 71 insertions(+), 13 deletions(-) diff --git a/examples/authors/sqlc.json b/examples/authors/sqlc.json index 105d591628..d93437a198 100644 --- a/examples/authors/sqlc.json +++ b/examples/authors/sqlc.json @@ -8,6 +8,9 @@ "database": { "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/authors" }, + "rules": [ + "db-prepare" + ], "gen": { "go": { "package": "authors", @@ -22,6 +25,9 @@ "database": { "url": "root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/authors?multiStatements=true&parseTime=true" }, + "rules": [ + "db-prepare" + ], "gen": { "go": { "package": "authors", @@ -36,6 +42,9 @@ "database": { "url": "file:authors?mode=memory&cache=shared" }, + "rules": [ + "db-prepare" + ], "gen": { "go": { "package": "authors", diff --git a/examples/batch/sqlc.json b/examples/batch/sqlc.json index 899d116f5f..857fd4378b 100644 --- a/examples/batch/sqlc.json +++ b/examples/batch/sqlc.json @@ -10,6 +10,9 @@ "database": { "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/batch" }, + "rules": [ + "db-prepare" + ], "sql_package": "pgx/v4", "emit_json_tags": true, "emit_prepared_queries": true, diff --git a/examples/booktest/sqlc.json b/examples/booktest/sqlc.json index da33d78f54..c634952cbd 100644 --- a/examples/booktest/sqlc.json +++ b/examples/booktest/sqlc.json @@ -9,7 +9,10 @@ "engine": "postgresql", "database": { "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/booktest" - } + }, + "rules": [ + "db-prepare" + ] }, { "name": "booktest", @@ -19,7 +22,10 @@ "engine": "mysql", "database": { "url": "root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/booktest?multiStatements=true&parseTime=true" - } + }, + "rules": [ + "db-prepare" + ] }, { "name": "booktest", @@ -29,7 +35,10 @@ "engine": "sqlite", "database": { "url": "file:booktest?mode=memory&cache=shared" - } + }, + "rules": [ + "db-prepare" + ] } ] } \ No newline at end of file diff --git a/examples/jets/sqlc.json b/examples/jets/sqlc.json index 412de61761..2327c41646 100644 --- a/examples/jets/sqlc.json +++ b/examples/jets/sqlc.json @@ -9,7 +9,10 @@ "engine": "postgresql", "database": { "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/jets" - } + }, + "rules": [ + "db-prepare" + ] } ] } diff --git a/examples/ondeck/sqlc.json b/examples/ondeck/sqlc.json index d6139b580c..b14d26d015 100644 --- a/examples/ondeck/sqlc.json +++ b/examples/ondeck/sqlc.json @@ -10,6 +10,9 @@ "database": { "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/ondeck" }, + "rules": [ + "db-prepare" + ], "emit_json_tags": true, "emit_prepared_queries": true, "emit_interface": true @@ -23,6 +26,9 @@ "database": { "url": "root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/ondeck?multiStatements=true&parseTime=true" }, + "rules": [ + "db-prepare" + ], "emit_json_tags": true, "emit_prepared_queries": true, "emit_interface": true @@ -36,6 +42,9 @@ "database": { "url": "file:ondeck?mode=memory&cache=shared" }, + "rules": [ + "db-prepare" + ], "emit_json_tags": true, "emit_prepared_queries": true, "emit_interface": true diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index f6e213300b..4aafb34636 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -14,6 +14,7 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/ext" "github.com/jackc/pgx/v5" _ "github.com/mattn/go-sqlite3" @@ -29,6 +30,8 @@ import ( var ErrFailedChecks = errors.New("failed checks") +const RuleDbPrepare = "db-prepare" + func NewCmdVet() *cobra.Command { return &cobra.Command{ Use: "vet", @@ -48,6 +51,17 @@ func NewCmdVet() *cobra.Command { } } +type emptyProgram struct { +} + +func (e *emptyProgram) Eval(any) (ref.Val, *cel.EvalDetails, error) { + return nil, nil, fmt.Errorf("unimplemented") +} + +func (e *emptyProgram) ContextEval(ctx context.Context, a any) (ref.Val, *cel.EvalDetails, error) { + return e.Eval(a) +} + func Vet(ctx context.Context, e Env, dir, filename string, stderr io.Writer) error { configPath, conf, err := readConfig(stderr, dir, filename) if err != nil { @@ -83,7 +97,9 @@ func Vet(ctx context.Context, e Env, dir, filename string, stderr io.Writer) err return fmt.Errorf("new env: %s", err) } - checks := map[string]cel.Program{} + checks := map[string]cel.Program{ + RuleDbPrepare: &emptyProgram{}, + } msgs := map[string]string{} for _, c := range conf.Rules { @@ -278,16 +294,21 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error { req := codeGenRequest(result, combo) cfg := vetConfig(req) for i, query := range req.Queries { - original := result.Queries[i] - if prep != nil && prepareable(s, original.RawStmt) { - name := fmt.Sprintf("sqlc_vet_%d_%d", time.Now().Unix(), i) - if err := prep.Prepare(ctx, name, query.Text); err != nil { - fmt.Fprintf(c.Stderr, "%s: error preparing %s on %s: %s\n", query.Filename, query.Name, s.Engine, err) - errored = true - } - } q := vetQuery(query) for _, name := range s.Rules { + // Built-in rule + if name == RuleDbPrepare { + original := result.Queries[i] + if prep != nil && prepareable(s, original.RawStmt) { + name := fmt.Sprintf("sqlc_vet_%d_%d", time.Now().Unix(), i) + if err := prep.Prepare(ctx, name, query.Text); err != nil { + fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: %s\n", query.Filename, q.Name, name, err) + errored = true + } + } + continue + } + prg, ok := c.Checks[name] if !ok { return fmt.Errorf("type-check error: a check with the name '%s' does not exist", name) diff --git a/internal/config/v_one.go b/internal/config/v_one.go index 122fe835d3..4ad96ea9eb 100644 --- a/internal/config/v_one.go +++ b/internal/config/v_one.go @@ -15,6 +15,7 @@ type V1GenerateSettings struct { Packages []v1PackageSettings `json:"packages" yaml:"packages"` Overrides []Override `json:"overrides,omitempty" yaml:"overrides,omitempty"` Rename map[string]string `json:"rename,omitempty" yaml:"rename,omitempty"` + Rules []Rule `json:"rules" yaml:"rules"` } type v1PackageSettings struct { @@ -51,6 +52,7 @@ type v1PackageSettings struct { StrictOrderBy *bool `json:"strict_order_by" yaml:"strict_order_by"` QueryParameterLimit *int32 `json:"query_parameter_limit,omitempty" yaml:"query_parameter_limit"` OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"` + Rules []string `json:"rules" yaml:"rules"` } func v1ParseConfig(rd io.Reader) (Config, error) { @@ -131,6 +133,7 @@ func (c *V1GenerateSettings) Translate() Config { Version: c.Version, Project: c.Project, Cloud: c.Cloud, + Rules: c.Rules, } for _, pkg := range c.Packages { @@ -143,6 +146,7 @@ func (c *V1GenerateSettings) Translate() Config { Database: pkg.Database, Schema: pkg.Schema, Queries: pkg.Queries, + Rules: pkg.Rules, Gen: SQLGen{ Go: &SQLGo{ EmitInterface: pkg.EmitInterface, From 9af6c1f78248d431b4141028bf90ae3797dfee45 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 29 Jun 2023 16:00:15 -0700 Subject: [PATCH 2/3] Error if db-prepare is used when a database connection isn't available --- internal/cmd/vet.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index 4aafb34636..9d17504b89 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -298,8 +298,13 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error { for _, name := range s.Rules { // Built-in rule if name == RuleDbPrepare { + if prep == nil { + fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: database connection required\n", query.Filename, q.Name, name) + errored = true + continue + } original := result.Queries[i] - if prep != nil && prepareable(s, original.RawStmt) { + if prepareable(s, original.RawStmt) { name := fmt.Sprintf("sqlc_vet_%d_%d", time.Now().Unix(), i) if err := prep.Prepare(ctx, name, query.Text); err != nil { fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: %s\n", query.Filename, q.Name, name, err) From fc532936fc08b17c4cf48f0c0faed3ea2036c85b Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 30 Jun 2023 14:10:40 -0700 Subject: [PATCH 3/3] namespace db-prepare --- examples/authors/sqlc.json | 56 ------------------------------------- examples/authors/sqlc.yaml | 35 +++++++++++++++++++++++ examples/batch/sqlc.json | 2 +- examples/booktest/sqlc.json | 6 ++-- examples/jets/sqlc.json | 2 +- examples/ondeck/sqlc.json | 6 ++-- internal/cmd/vet.go | 2 +- 7 files changed, 44 insertions(+), 65 deletions(-) delete mode 100644 examples/authors/sqlc.json create mode 100644 examples/authors/sqlc.yaml diff --git a/examples/authors/sqlc.json b/examples/authors/sqlc.json deleted file mode 100644 index d93437a198..0000000000 --- a/examples/authors/sqlc.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "version": "2", - "sql": [ - { - "schema": "postgresql/schema.sql", - "queries": "postgresql/query.sql", - "engine": "postgresql", - "database": { - "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/authors" - }, - "rules": [ - "db-prepare" - ], - "gen": { - "go": { - "package": "authors", - "out": "postgresql" - } - } - }, - { - "schema": "mysql/schema.sql", - "queries": "mysql/query.sql", - "engine": "mysql", - "database": { - "url": "root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/authors?multiStatements=true&parseTime=true" - }, - "rules": [ - "db-prepare" - ], - "gen": { - "go": { - "package": "authors", - "out": "mysql" - } - } - }, - { - "schema": "sqlite/schema.sql", - "queries": "sqlite/query.sql", - "engine": "sqlite", - "database": { - "url": "file:authors?mode=memory&cache=shared" - }, - "rules": [ - "db-prepare" - ], - "gen": { - "go": { - "package": "authors", - "out": "sqlite" - } - } - } - ] -} \ No newline at end of file diff --git a/examples/authors/sqlc.yaml b/examples/authors/sqlc.yaml new file mode 100644 index 0000000000..a3995835fa --- /dev/null +++ b/examples/authors/sqlc.yaml @@ -0,0 +1,35 @@ +version: '2' +sql: +- schema: postgresql/schema.sql + queries: postgresql/query.sql + engine: postgresql + database: + url: postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/authors + rules: + - sqlc/db-prepare + gen: + go: + package: authors + out: postgresql +- schema: mysql/schema.sql + queries: mysql/query.sql + engine: mysql + database: + url: root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/authors?multiStatements=true&parseTime=true + rules: + - sqlc/db-prepare + gen: + go: + package: authors + out: mysql +- schema: sqlite/schema.sql + queries: sqlite/query.sql + engine: sqlite + database: + url: file:authors?mode=memory&cache=shared + rules: + - sqlc/db-prepare + gen: + go: + package: authors + out: sqlite \ No newline at end of file diff --git a/examples/batch/sqlc.json b/examples/batch/sqlc.json index 857fd4378b..d19c3cfc13 100644 --- a/examples/batch/sqlc.json +++ b/examples/batch/sqlc.json @@ -11,7 +11,7 @@ "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/batch" }, "rules": [ - "db-prepare" + "sqlc/db-prepare" ], "sql_package": "pgx/v4", "emit_json_tags": true, diff --git a/examples/booktest/sqlc.json b/examples/booktest/sqlc.json index c634952cbd..64fc96eea7 100644 --- a/examples/booktest/sqlc.json +++ b/examples/booktest/sqlc.json @@ -11,7 +11,7 @@ "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/booktest" }, "rules": [ - "db-prepare" + "sqlc/db-prepare" ] }, { @@ -24,7 +24,7 @@ "url": "root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/booktest?multiStatements=true&parseTime=true" }, "rules": [ - "db-prepare" + "sqlc/db-prepare" ] }, { @@ -37,7 +37,7 @@ "url": "file:booktest?mode=memory&cache=shared" }, "rules": [ - "db-prepare" + "sqlc/db-prepare" ] } ] diff --git a/examples/jets/sqlc.json b/examples/jets/sqlc.json index 2327c41646..c29494bd4b 100644 --- a/examples/jets/sqlc.json +++ b/examples/jets/sqlc.json @@ -11,7 +11,7 @@ "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/jets" }, "rules": [ - "db-prepare" + "sqlc/db-prepare" ] } ] diff --git a/examples/ondeck/sqlc.json b/examples/ondeck/sqlc.json index b14d26d015..e0896eed70 100644 --- a/examples/ondeck/sqlc.json +++ b/examples/ondeck/sqlc.json @@ -11,7 +11,7 @@ "url": "postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/ondeck" }, "rules": [ - "db-prepare" + "sqlc/db-prepare" ], "emit_json_tags": true, "emit_prepared_queries": true, @@ -27,7 +27,7 @@ "url": "root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/ondeck?multiStatements=true&parseTime=true" }, "rules": [ - "db-prepare" + "sqlc/db-prepare" ], "emit_json_tags": true, "emit_prepared_queries": true, @@ -43,7 +43,7 @@ "url": "file:ondeck?mode=memory&cache=shared" }, "rules": [ - "db-prepare" + "sqlc/db-prepare" ], "emit_json_tags": true, "emit_prepared_queries": true, diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index 9d17504b89..19040cf8ce 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -30,7 +30,7 @@ import ( var ErrFailedChecks = errors.New("failed checks") -const RuleDbPrepare = "db-prepare" +const RuleDbPrepare = "sqlc/db-prepare" func NewCmdVet() *cobra.Command { return &cobra.Command{