diff --git a/examples/authors/sqlc.json b/examples/authors/sqlc.json index 303e2b03a3..558ea97cbe 100644 --- a/examples/authors/sqlc.json +++ b/examples/authors/sqlc.json @@ -5,6 +5,9 @@ "schema": "postgresql/schema.sql", "queries": "postgresql/query.sql", "engine": "postgresql", + "database": { + "url": "'postgresql://%s:%s@%s:%s/authors'.format([env.PG_USER, env.PG_PASSWORD, env.PG_HOST, env.PG_PORT])" + }, "gen": { "go": { "package": "authors", diff --git a/examples/batch/sqlc.json b/examples/batch/sqlc.json index f56bad9b5d..b994f3b945 100644 --- a/examples/batch/sqlc.json +++ b/examples/batch/sqlc.json @@ -7,6 +7,9 @@ "schema": "postgresql/schema.sql", "queries": "postgresql/query.sql", "engine": "postgresql", + "database": { + "url": "'postgresql://%s:%s@%s:%s/batch'.format([env.PG_USER, env.PG_PASSWORD, env.PG_HOST, env.PG_PORT])" + }, "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 d4a0be3a59..c0176d1f23 100644 --- a/examples/booktest/sqlc.json +++ b/examples/booktest/sqlc.json @@ -6,7 +6,10 @@ "path": "postgresql", "schema": "postgresql/schema.sql", "queries": "postgresql/query.sql", - "engine": "postgresql" + "engine": "postgresql", + "database": { + "url": "'postgresql://%s:%s@%s:%s/booktest'.format([env.PG_USER, env.PG_PASSWORD, env.PG_HOST, env.PG_PORT])" + } }, { "name": "booktest", diff --git a/examples/jets/db.go b/examples/jets/postgresql/db.go similarity index 100% rename from examples/jets/db.go rename to examples/jets/postgresql/db.go diff --git a/examples/jets/models.go b/examples/jets/postgresql/models.go similarity index 100% rename from examples/jets/models.go rename to examples/jets/postgresql/models.go diff --git a/examples/jets/query-building.sql b/examples/jets/postgresql/query-building.sql similarity index 100% rename from examples/jets/query-building.sql rename to examples/jets/postgresql/query-building.sql diff --git a/examples/jets/query-building.sql.go b/examples/jets/postgresql/query-building.sql.go similarity index 100% rename from examples/jets/query-building.sql.go rename to examples/jets/postgresql/query-building.sql.go diff --git a/examples/jets/schema.sql b/examples/jets/postgresql/schema.sql similarity index 100% rename from examples/jets/schema.sql rename to examples/jets/postgresql/schema.sql diff --git a/examples/jets/sqlc.json b/examples/jets/sqlc.json index 0bca6f48df..7b15009422 100644 --- a/examples/jets/sqlc.json +++ b/examples/jets/sqlc.json @@ -2,11 +2,14 @@ "version": "1", "packages": [ { - "path": ".", + "path": "postgresql", "name": "jets", - "schema": "schema.sql", - "queries": "query-building.sql", - "engine": "postgresql" + "schema": "postgresql/schema.sql", + "queries": "postgresql/query-building.sql", + "engine": "postgresql", + "database": { + "url": "'postgresql://%s:%s@%s:%s/jets'.format([env.PG_USER, env.PG_PASSWORD, env.PG_HOST, env.PG_PORT])" + } } ] } diff --git a/examples/kotlin/sqlc.json b/examples/kotlin/sqlc.json index 720f77ab88..284b76ec91 100644 --- a/examples/kotlin/sqlc.json +++ b/examples/kotlin/sqlc.json @@ -39,8 +39,8 @@ ] }, { - "schema": "src/main/resources/jets/schema.sql", - "queries": "src/main/resources/jets/query-building.sql", + "schema": "src/main/resources/jets/postgresql/schema.sql", + "queries": "src/main/resources/jets/postgresql/query-building.sql", "engine": "postgresql", "codegen": [ { diff --git a/examples/ondeck/sqlc.json b/examples/ondeck/sqlc.json index 7ad4bb3fc4..f3ae36698c 100644 --- a/examples/ondeck/sqlc.json +++ b/examples/ondeck/sqlc.json @@ -7,6 +7,9 @@ "schema": "postgresql/schema", "queries": "postgresql/query", "engine": "postgresql", + "database": { + "url": "'postgresql://%s:%s@%s:%s/ondeck'.format([env.PG_USER, env.PG_PASSWORD, env.PG_HOST, env.PG_PORT])" + }, "emit_json_tags": true, "emit_prepared_queries": true, "emit_interface": true diff --git a/examples/python/sqlc.json b/examples/python/sqlc.json index cb84d83503..29d3cc8464 100644 --- a/examples/python/sqlc.json +++ b/examples/python/sqlc.json @@ -44,8 +44,8 @@ ] }, { - "schema": "../jets/schema.sql", - "queries": "../jets/query-building.sql", + "schema": "../jets/postgresql/schema.sql", + "queries": "../jets/postgresql/query-building.sql", "engine": "postgresql", "codegen": [ { diff --git a/go.mod b/go.mod index 5be6701acc..2cec9d1909 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/google/go-cmp v0.5.9 github.com/jackc/pgconn v1.14.0 github.com/jackc/pgx/v4 v4.18.1 + github.com/jackc/pgx/v5 v5.4.1 github.com/jinzhu/inflection v1.0.0 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.17 @@ -25,7 +26,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/stoewer/go-strcase v1.2.0 // indirect +require ( + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect +) require ( github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect @@ -45,10 +49,10 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.19.1 // indirect - golang.org/x/crypto v0.6.0 // indirect + golang.org/x/crypto v0.9.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index fd624928ef..5926d85c7e 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= +github.com/jackc/pgx/v5 v5.4.1 h1:oKfB/FhuVtit1bBM3zNRRsZ925ZkMN3HXL+LgLUM9lE= +github.com/jackc/pgx/v5 v5.4.1/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -102,6 +104,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= @@ -119,7 +122,6 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/pganalyze/pg_query_go/v4 v4.2.1 h1:id/vuyIQccb9f6Yx3pzH5l4QYrxE3v6/m8RPlgMrprc= github.com/pganalyze/pg_query_go/v4 v4.2.1/go.mod h1:aEkDNOXNM5j0YGzaAapwJ7LB3dLNj+bvbWcLv1hOVqA= github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= @@ -139,6 +141,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ= github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 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.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -205,8 +209,9 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -222,8 +227,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -246,8 +251,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -295,7 +300,7 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index d9f69901d8..f034b057aa 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -9,14 +9,18 @@ import ( "path/filepath" "runtime/trace" "strings" + "time" "github.com/google/cel-go/cel" + "github.com/google/cel-go/ext" + "github.com/jackc/pgx/v5" "github.com/spf13/cobra" "github.com/kyleconroy/sqlc/internal/config" "github.com/kyleconroy/sqlc/internal/debug" "github.com/kyleconroy/sqlc/internal/opts" "github.com/kyleconroy/sqlc/internal/plugin" + "github.com/kyleconroy/sqlc/internal/sql/ast" ) var ErrFailedChecks = errors.New("failed checks") @@ -59,6 +63,7 @@ func Vet(ctx context.Context, e Env, dir, filename string, stderr io.Writer) err env, err := cel.NewEnv( cel.StdLib(), + ext.Strings(ext.StringsVersion(1)), cel.Types( &plugin.VetConfig{}, &plugin.VetQuery{}, @@ -71,7 +76,7 @@ func Vet(ctx context.Context, e Env, dir, filename string, stderr io.Writer) err ), ) if err != nil { - return fmt.Errorf("new env; %s", err) + return fmt.Errorf("new env: %s", err) } checks := map[string]cel.Program{} @@ -99,62 +104,181 @@ func Vet(ctx context.Context, e Env, dir, filename string, stderr io.Writer) err msgs[c.Name] = c.Msg } - errored := true - for _, sql := range conf.SQL { - combo := config.Combine(*conf, sql) + dbenv, err := cel.NewEnv( + cel.StdLib(), + ext.Strings(ext.StringsVersion(1)), + cel.Variable("env", + cel.MapType(cel.StringType, cel.StringType), + ), + ) + if err != nil { + return fmt.Errorf("new dbenv; %s", err) + } - // TODO: This feels like a hack that will bite us later - joined := make([]string, 0, len(sql.Schema)) - for _, s := range sql.Schema { - joined = append(joined, filepath.Join(dir, s)) + c := checker{ + Checks: checks, + Conf: conf, + Dbenv: dbenv, + Dir: dir, + Env: env, + Envmap: map[string]string{}, + Msgs: msgs, + Stderr: stderr, + } + errored := false + for _, sql := range conf.SQL { + if err := c.checkSQL(ctx, sql); err != nil { + if !errors.Is(err, ErrFailedChecks) { + fmt.Fprintf(stderr, "%s\n", err) + } + errored = true } - sql.Schema = joined + } + if errored { + return ErrFailedChecks + } + return nil +} + +type checker struct { + Checks map[string]cel.Program + Conf *config.Config + Dbenv *cel.Env + Dir string + Env *cel.Env + Envmap map[string]string + Msgs map[string]string + Stderr io.Writer +} - joined = make([]string, 0, len(sql.Queries)) - for _, q := range sql.Queries { - joined = append(joined, filepath.Join(dir, q)) +// Determine if a query can be prepared based on the engine and the statement +// type. +func prepareable(sql config.SQL, raw *ast.RawStmt) bool { + if sql.Engine == config.EnginePostgreSQL { + // TOOD: Add support for MERGE and VALUES stmts + switch raw.Stmt.(type) { + case *ast.DeleteStmt: + return true + case *ast.InsertStmt: + return true + case *ast.SelectStmt: + return true + case *ast.UpdateStmt: + return true + default: + return false } - sql.Queries = joined + } + return false +} + +func (c *checker) checkSQL(ctx context.Context, sql config.SQL) error { + // TODO: Create a separate function for this logic so we can + combo := config.Combine(*c.Conf, sql) + + // TODO: This feels like a hack that will bite us later + joined := make([]string, 0, len(sql.Schema)) + for _, s := range sql.Schema { + joined = append(joined, filepath.Join(c.Dir, s)) + } + sql.Schema = joined + + joined = make([]string, 0, len(sql.Queries)) + for _, q := range sql.Queries { + joined = append(joined, filepath.Join(c.Dir, q)) + } + sql.Queries = joined + + var name string + parseOpts := opts.Parser{ + Debug: debug.Debug, + } + + result, failed := parse(ctx, name, c.Dir, sql, combo, parseOpts, c.Stderr) + if failed { + return ErrFailedChecks + } - var name string - parseOpts := opts.Parser{ - Debug: debug.Debug, + // TODO: Add MySQL support + var pgconn *pgx.Conn + if sql.Engine == config.EnginePostgreSQL && sql.Database != nil { + ast, issues := c.Dbenv.Compile(sql.Database.URL) + if issues != nil && issues.Err() != nil { + return fmt.Errorf("type-check error: database url %s", issues.Err()) + } + prg, err := c.Dbenv.Program(ast) + if err != nil { + return fmt.Errorf("program construction error: database url %s", err) + } + // Populate the environment variable map if it is empty + if len(c.Envmap) == 0 { + for _, e := range os.Environ() { + k, v, _ := strings.Cut(e, "=") + c.Envmap[k] = v + } + } + out, _, err := prg.Eval(map[string]any{ + "env": c.Envmap, + }) + if err != nil { + return fmt.Errorf("expression error: %s", err) + } + dburl, ok := out.Value().(string) + if !ok { + return fmt.Errorf("expression returned non-string value: %v", out.Value()) } + fmt.Println("URL", dburl) + conn, err := pgx.Connect(ctx, dburl) + if err != nil { + return fmt.Errorf("database: connection error: %s", err) + } + if err := conn.Ping(ctx); err != nil { + return fmt.Errorf("database: connection error: %s", err) + } + defer conn.Close(ctx) + pgconn = conn + } - result, failed := parse(ctx, name, dir, sql, combo, parseOpts, stderr) - if failed { - return nil + errored := false + req := codeGenRequest(result, combo) + cfg := vetConfig(req) + for i, query := range req.Queries { + original := result.Queries[i] + if pgconn != nil && prepareable(sql, original.RawStmt) { + name := fmt.Sprintf("sqlc_vet_%d_%d", time.Now().Unix(), i) + _, err := pgconn.Prepare(ctx, name, query.Text) + if err != nil { + fmt.Fprintf(c.Stderr, "%s: error preparing %s: %s\n", query.Filename, query.Name, err) + errored = true + continue + } } - req := codeGenRequest(result, combo) - cfg := vetConfig(req) - for _, query := range req.Queries { - q := vetQuery(query) - for _, name := range sql.Rules { - prg, ok := checks[name] - if !ok { - return fmt.Errorf("type-check error: a check with the name '%s' does not exist", name) - } - out, _, err := prg.Eval(map[string]any{ - "query": q, - "config": cfg, - }) - if err != nil { - return err - } - tripped, ok := out.Value().(bool) - if !ok { - return fmt.Errorf("expression returned non-bool: %s", err) - } - if tripped { - // TODO: Get line numbers in the output - msg := msgs[name] - if msg == "" { - fmt.Fprintf(stderr, query.Filename+": %s: %s\n", q.Name, name, msg) - } else { - fmt.Fprintf(stderr, query.Filename+": %s: %s: %s\n", q.Name, name, msg) - } - errored = true + q := vetQuery(query) + for _, name := range sql.Rules { + prg, ok := c.Checks[name] + if !ok { + return fmt.Errorf("type-check error: a check with the name '%s' does not exist", name) + } + out, _, err := prg.Eval(map[string]any{ + "query": q, + "config": cfg, + }) + if err != nil { + return err + } + tripped, ok := out.Value().(bool) + if !ok { + return fmt.Errorf("expression returned non-bool value: %v", out.Value()) + } + if tripped { + // TODO: Get line numbers in the output + msg := c.Msgs[name] + if msg == "" { + fmt.Fprintf(c.Stderr, "%s: %s: %s\n", query.Filename, q.Name, name) + } else { + fmt.Fprintf(c.Stderr, "%s: %s: %s: %s\n", query.Filename, q.Name, name, msg) } + errored = true } } } diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go index 9ac5cc855a..b108acc492 100644 --- a/internal/compiler/parse.go +++ b/internal/compiler/parse.go @@ -126,6 +126,7 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, return nil, err } return &Query{ + RawStmt: raw, Cmd: cmd, Comments: comments, Name: name, diff --git a/internal/compiler/query.go b/internal/compiler/query.go index c3e754cc04..e77f555dbd 100644 --- a/internal/compiler/query.go +++ b/internal/compiler/query.go @@ -51,6 +51,9 @@ type Query struct { // Needed for CopyFrom InsertIntoTable *ast.TableName + + // Needed for vet + RawStmt *ast.RawStmt } type Parameter struct { diff --git a/internal/config/config.go b/internal/config/config.go index 9acadd8355..1d7df3111e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "errors" - "fmt" "io" "gopkg.in/yaml.v3" @@ -69,6 +68,10 @@ type Project struct { ID string `json:"id" yaml:"id"` } +type Database struct { + URL string `json:"url" yaml:"url"` +} + type Cloud struct { Organization string `json:"organization" yaml:"organization"` Project string `json:"project" yaml:"project"` @@ -105,6 +108,7 @@ type SQL struct { Engine Engine `json:"engine,omitempty" yaml:"engine"` Schema Paths `json:"schema" yaml:"schema"` Queries Paths `json:"queries" yaml:"queries"` + Database *Database `json:"database" yaml:"database"` StrictFunctionChecks bool `json:"strict_function_checks" yaml:"strict_function_checks"` StrictOrderBy *bool `json:"strict_order_by" yaml:"strict_order_by"` Gen SQLGen `json:"gen" yaml:"gen"` @@ -204,19 +208,6 @@ func ParseConfig(rd io.Reader) (Config, error) { } } -func Validate(c *Config) error { - for _, sql := range c.SQL { - sqlGo := sql.Gen.Go - if sqlGo == nil { - continue - } - if sqlGo.EmitMethodsWithDBArgument && sqlGo.EmitPreparedQueries { - return fmt.Errorf("invalid config: emit_methods_with_db_argument and emit_prepared_queries settings are mutually exclusive") - } - } - return nil -} - type CombinedSettings struct { Global Config Package SQL diff --git a/internal/config/v_one.go b/internal/config/v_one.go index 273413aea4..122fe835d3 100644 --- a/internal/config/v_one.go +++ b/internal/config/v_one.go @@ -20,6 +20,7 @@ type V1GenerateSettings struct { type v1PackageSettings struct { Name string `json:"name" yaml:"name"` Engine Engine `json:"engine,omitempty" yaml:"engine"` + Database *Database `json:"database,omitempty" yaml:"database"` Path string `json:"path" yaml:"path"` Schema Paths `json:"schema" yaml:"schema"` Queries Paths `json:"queries" yaml:"queries"` @@ -138,9 +139,10 @@ func (c *V1GenerateSettings) Translate() Config { pkg.StrictOrderBy = &defaultValue } conf.SQL = append(conf.SQL, SQL{ - Engine: pkg.Engine, - Schema: pkg.Schema, - Queries: pkg.Queries, + Engine: pkg.Engine, + Database: pkg.Database, + Schema: pkg.Schema, + Queries: pkg.Queries, Gen: SQLGen{ Go: &SQLGo{ EmitInterface: pkg.EmitInterface, diff --git a/internal/config/validate.go b/internal/config/validate.go new file mode 100644 index 0000000000..4810a32eb3 --- /dev/null +++ b/internal/config/validate.go @@ -0,0 +1,21 @@ +package config + +import "fmt" + +func Validate(c *Config) error { + for _, sql := range c.SQL { + sqlGo := sql.Gen.Go + if sqlGo == nil { + continue + } + if sqlGo.EmitMethodsWithDBArgument && sqlGo.EmitPreparedQueries { + return fmt.Errorf("invalid config: emit_methods_with_db_argument and emit_prepared_queries settings are mutually exclusive") + } + if sql.Database != nil { + if sql.Database.URL == "" { + return fmt.Errorf("invalid config: database must have a non-empty URL") + } + } + } + return nil +} diff --git a/internal/endtoend/vet_test.go b/internal/endtoend/vet_test.go new file mode 100644 index 0000000000..94108107dc --- /dev/null +++ b/internal/endtoend/vet_test.go @@ -0,0 +1,67 @@ +//go:build examples +// +build examples + +package main + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + + "github.com/kyleconroy/sqlc/internal/cmd" + "github.com/kyleconroy/sqlc/internal/sqltest" +) + +func findSchema(t *testing.T, path string) string { + t.Helper() + schemaFile := filepath.Join(path, "postgresql", "schema.sql") + if _, err := os.Stat(schemaFile); !os.IsNotExist(err) { + return schemaFile + } + schemaDir := filepath.Join(path, "postgresql", "schema") + if _, err := os.Stat(schemaDir); !os.IsNotExist(err) { + return schemaDir + } + t.Fatalf("error: can't find schema files in %s", path) + return "" +} + +func TestExamplesVet(t *testing.T) { + t.Parallel() + ctx := context.Background() + + examples, err := filepath.Abs(filepath.Join("..", "..", "examples")) + if err != nil { + t.Fatal(err) + } + + files, err := os.ReadDir(examples) + if err != nil { + t.Fatal(err) + } + + for _, replay := range files { + if !replay.IsDir() { + continue + } + tc := replay.Name() + t.Run(tc, func(t *testing.T) { + t.Parallel() + path := filepath.Join(examples, tc) + + if tc != "kotlin" && tc != "python" { + sqltest.CreatePostgreSQLDatabase(t, tc, []string{ + findSchema(t, path), + }) + } + + var stderr bytes.Buffer + err := cmd.Vet(ctx, cmd.Env{}, path, "", &stderr) + if err != nil { + t.Fatalf("sqlc vet failed: %s %s", err, stderr.String()) + } + }) + } +} diff --git a/internal/sqltest/postgres.go b/internal/sqltest/postgres.go index 3968b6bddb..ad58e2c019 100644 --- a/internal/sqltest/postgres.go +++ b/internal/sqltest/postgres.go @@ -28,6 +28,86 @@ func id() string { return string(b) } +// Disable random new schema +// Override database name +func CreatePostgreSQLDatabase(t *testing.T, newDB string, migrations []string) *sql.DB { + t.Helper() + + pgUser := os.Getenv("PG_USER") + pgHost := os.Getenv("PG_HOST") + pgPort := os.Getenv("PG_PORT") + pgPass := os.Getenv("PG_PASSWORD") + pgDB := os.Getenv("PG_DATABASE") + + if pgUser == "" { + pgUser = "postgres" + } + + if pgPass == "" { + pgPass = "mysecretpassword" + } + + if pgPort == "" { + pgPort = "5432" + } + + if pgHost == "" { + pgHost = "127.0.0.1" + } + + if pgDB == "" { + pgDB = "dinotest" + } + + source := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", pgUser, pgPass, pgHost, pgPort, pgDB) + t.Logf("db: %s", source) + + db, err := sql.Open("postgres", source) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + var exists bool + dberr := db.QueryRow(`SELECT true FROM pg_database WHERE datname = $1`, newDB).Scan(&exists) + if dberr != nil && dberr != sql.ErrNoRows { + t.Fatal(err) + } + + if !exists { + if _, err := db.Exec("CREATE DATABASE " + newDB); err != nil { + t.Fatal(err) + } + } else { + t.Logf("database '%s' exists, not creating", newDB) + } + + newSource := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", pgUser, pgPass, pgHost, pgPort, newDB) + t.Logf("newdb: %s", newSource) + + sdb, err := sql.Open("postgres", newSource) + if err != nil { + t.Fatal(err) + } + + if !exists { + files, err := sqlpath.Glob(migrations) + if err != nil { + t.Fatal(err) + } + for _, f := range files { + blob, err := os.ReadFile(f) + if err != nil { + t.Fatal(err) + } + if _, err := sdb.Exec(string(blob)); err != nil { + t.Fatalf("%s: %s", filepath.Base(f), err) + } + } + } + return sdb +} + func PostgreSQL(t *testing.T, migrations []string) (*sql.DB, func()) { t.Helper()