diff --git a/.github/workflows/ci-kotlin.yml b/.github/workflows/ci-kotlin.yml index 835f97fbd6..af9563038d 100644 --- a/.github/workflows/ci-kotlin.yml +++ b/.github/workflows/ci-kotlin.yml @@ -30,7 +30,7 @@ jobs: - 3306:3306 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: actions/setup-java@v2 with: distribution: 'adopt' diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index 846f57e630..9fb0d5143b 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -23,7 +23,7 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: actions/setup-python@v2 with: python-version: 3.9 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 322fa5537e..63965874a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - 3306:3306 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: actions/setup-go@v2 with: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2bebc327be..532ac1a8bc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,13 +12,13 @@ jobs: id: prep run: | echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: docker/setup-buildx-action@v1 - - uses: docker/login-action@v1 + - uses: docker/login-action@v1.9.0 with: username: kjconroy password: ${{ secrets.DOCKER_PASSWORD }} - - uses: docker/build-push-action@v2 + - uses: docker/build-push-action@v2.4.0 with: context: . file: ./Dockerfile diff --git a/.github/workflows/equinox.yml b/.github/workflows/equinox.yml index 2a41f0d11d..f8521ffa41 100644 --- a/.github/workflows/equinox.yml +++ b/.github/workflows/equinox.yml @@ -11,7 +11,7 @@ jobs: name: release --platforms windows runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: actions/setup-go@v2 with: go-version: '1.16' @@ -26,7 +26,7 @@ jobs: name: release --platforms darwin runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: actions/setup-go@v2 with: go-version: '1.16' @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest needs: [macos, windows] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: actions/setup-go@v2 with: go-version: '1.16' diff --git a/Dockerfile b/Dockerfile index 4a4d7732b3..b457a6e2b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # STEP 1: Build sqlc -FROM golang:1.16.3 AS builder +FROM golang:1.16.4 AS builder COPY . /workspace WORKDIR /workspace diff --git a/examples/booktest/postgresql/db_test.go b/examples/booktest/postgresql/db_test.go index b5c6cd4c33..dcc24c05f8 100644 --- a/examples/booktest/postgresql/db_test.go +++ b/examples/booktest/postgresql/db_test.go @@ -139,7 +139,7 @@ func TestBooks(t *testing.T) { t.Fatal(err) } for _, ab := range res { - t.Logf("Book %d: '%s', Author: '%s', ISBN: '%s' Tags: '%v'\n", ab.BookID, ab.Title, ab.Name, ab.Isbn, ab.Tags) + t.Logf("Book %d: '%s', Author: '%s', ISBN: '%s' Tags: '%v'\n", ab.BookID, ab.Title, ab.Name.String, ab.Isbn, ab.Tags) } // TODO: call say_hello(varchar) diff --git a/examples/booktest/postgresql/query.sql.go b/examples/booktest/postgresql/query.sql.go index ff690ed6c8..a4cc01469b 100644 --- a/examples/booktest/postgresql/query.sql.go +++ b/examples/booktest/postgresql/query.sql.go @@ -5,6 +5,7 @@ package booktest import ( "context" + "database/sql" "time" "github.com/lib/pq" @@ -25,7 +26,7 @@ WHERE tags && $1::varchar[] type BooksByTagsRow struct { BookID int32 Title string - Name string + Name sql.NullString Isbn string Tags []string } diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt index f65e6def25..fbf94fba34 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -23,7 +23,7 @@ WHERE tags && ?::varchar[] data class BooksByTagsRow ( val bookId: Int, val title: String, - val name: String, + val name: String?, val isbn: String, val tags: List ) diff --git a/examples/python/src/booktest/query.py b/examples/python/src/booktest/query.py index 6bc73be5fb..ea8255237f 100644 --- a/examples/python/src/booktest/query.py +++ b/examples/python/src/booktest/query.py @@ -27,7 +27,7 @@ class BooksByTagsRow: book_id: int title: str - name: str + name: Optional[str] isbn: str tags: List[str] diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index 0e8329e274..56e236cecc 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -206,9 +206,57 @@ func outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, error) { } } + if n, ok := node.(*ast.SelectStmt); ok { + for _, col := range cols { + if !col.NotNull || col.Table == nil { + continue + } + for _, f := range n.FromClause.Items { + if res := isTableRequired(f, col.Table.Name, tableRequired); res != tableNotFound { + col.NotNull = res == tableRequired + break + } + } + } + } + return cols, nil } +const ( + tableNotFound = iota + tableRequired + tableOptional +) + +func isTableRequired(n ast.Node, tableName string, prior int) int { + switch n := n.(type) { + case *ast.RangeVar: + if *n.Relname == tableName { + return prior + } + case *ast.JoinExpr: + helper := func(l, r int) int { + if res := isTableRequired(n.Larg, tableName, l); res != tableNotFound { + return res + } + if res := isTableRequired(n.Rarg, tableName, r); res != tableNotFound { + return res + } + return tableNotFound + } + switch n.Jointype { + case ast.JoinTypeLeft: + return helper(tableRequired, tableOptional) + case ast.JoinTypeRight: + return helper(tableOptional, tableRequired) + case ast.JoinTypeFull: + return helper(tableOptional, tableOptional) + } + } + return tableNotFound +} + // Compute the output columns for a statement. // // Return an error if column references are ambiguous @@ -251,10 +299,13 @@ func sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) { var tables []*Table for _, item := range list.Items { switch n := item.(type) { + case *ast.FuncName: + // If the function or table can't be found, don't error out. There + // are many queries that depend on functions unknown to sqlc. fn, err := qc.GetFunc(n) if err != nil { - return nil, err + continue } table, err := qc.GetTable(&ast.TableName{ Catalog: fn.ReturnType.Catalog, @@ -262,9 +313,10 @@ func sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) { Name: fn.ReturnType.Name, }) if err != nil { - return nil, err + continue } tables = append(tables, table) + case *ast.RangeSubselect: cols, err := outputColumns(qc, n.Subquery) if err != nil { @@ -297,6 +349,7 @@ func sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) { } } tables = append(tables, table) + default: return nil, fmt.Errorf("sourceTable: unsupported list item type: %T", n) } diff --git a/internal/endtoend/testdata/func_return/posgresql/pgx/query.sql b/internal/endtoend/testdata/func_return/posgresql/pgx/query.sql index f72450421a..cfea2119d0 100644 --- a/internal/endtoend/testdata/func_return/posgresql/pgx/query.sql +++ b/internal/endtoend/testdata/func_return/posgresql/pgx/query.sql @@ -2,3 +2,8 @@ SELECT * FROM users_func() WHERE first_name != ''; + +/* name: GenerateSeries :many */ +SELECT ($1::inet) + i +FROM generate_series(0, $2::int) AS i +LIMIT 1; diff --git a/internal/endtoend/testdata/func_return/posgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/func_return/posgresql/stdlib/go/query.sql.go index f40859f8e1..990f335916 100644 --- a/internal/endtoend/testdata/func_return/posgresql/stdlib/go/query.sql.go +++ b/internal/endtoend/testdata/func_return/posgresql/stdlib/go/query.sql.go @@ -5,8 +5,43 @@ package querytest import ( "context" + "net" ) +const generateSeries = `-- name: GenerateSeries :many +SELECT ($1::inet) + i +FROM generate_series(0, $2::int) AS i +LIMIT 1 +` + +type GenerateSeriesParams struct { + Column1 net.IP + Column2 int32 +} + +func (q *Queries) GenerateSeries(ctx context.Context, arg GenerateSeriesParams) ([]int32, error) { + rows, err := q.db.QueryContext(ctx, generateSeries, arg.Column1, arg.Column2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int32 + for rows.Next() { + var column_1 int32 + if err := rows.Scan(&column_1); err != nil { + return nil, err + } + items = append(items, column_1) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getUsers = `-- name: GetUsers :many SELECT id, first_name FROM users_func() diff --git a/internal/endtoend/testdata/join_full/mysql/go/db.go b/internal/endtoend/testdata/join_full/mysql/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/join_full/mysql/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/join_full/mysql/go/models.go b/internal/endtoend/testdata/join_full/mysql/go/models.go new file mode 100644 index 0000000000..d7b3dd5a30 --- /dev/null +++ b/internal/endtoend/testdata/join_full/mysql/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type Bar struct { + ID int32 +} + +type Foo struct { + ID int32 + BarID sql.NullInt32 +} diff --git a/internal/endtoend/testdata/join_full/mysql/go/query.sql.go b/internal/endtoend/testdata/join_full/mysql/go/query.sql.go new file mode 100644 index 0000000000..fbc977ad37 --- /dev/null +++ b/internal/endtoend/testdata/join_full/mysql/go/query.sql.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const fullJoin = `-- name: FullJoin :many +SELECT f.id, f.bar_id, b.id +FROM foo f +FULL OUTER JOIN bar b ON b.id = f.bar_id +WHERE f.id = $1 +` + +type FullJoinRow struct { + ID sql.NullInt32 + BarID sql.NullInt32 + ID_2 sql.NullInt32 +} + +func (q *Queries) FullJoin(ctx context.Context, id int32) ([]FullJoinRow, error) { + rows, err := q.db.QueryContext(ctx, fullJoin, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FullJoinRow + for rows.Next() { + var i FullJoinRow + if err := rows.Scan(&i.ID, &i.BarID, &i.ID_2); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/join_full/mysql/query.sql b/internal/endtoend/testdata/join_full/mysql/query.sql new file mode 100644 index 0000000000..76abe8a07c --- /dev/null +++ b/internal/endtoend/testdata/join_full/mysql/query.sql @@ -0,0 +1,8 @@ +CREATE TABLE foo (id serial not null, bar_id int references bar(id)); +CREATE TABLE bar (id serial not null); + +-- name: FullJoin :many +SELECT f.id, f.bar_id, b.id +FROM foo f +FULL OUTER JOIN bar b ON b.id = f.bar_id +WHERE f.id = $1; \ No newline at end of file diff --git a/internal/endtoend/testdata/join_full/mysql/sqlc.json b/internal/endtoend/testdata/join_full/mysql/sqlc.json new file mode 100644 index 0000000000..c72b6132d5 --- /dev/null +++ b/internal/endtoend/testdata/join_full/mysql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/join_full/postgresql/go/db.go b/internal/endtoend/testdata/join_full/postgresql/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/join_full/postgresql/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/join_full/postgresql/go/models.go b/internal/endtoend/testdata/join_full/postgresql/go/models.go new file mode 100644 index 0000000000..d7b3dd5a30 --- /dev/null +++ b/internal/endtoend/testdata/join_full/postgresql/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type Bar struct { + ID int32 +} + +type Foo struct { + ID int32 + BarID sql.NullInt32 +} diff --git a/internal/endtoend/testdata/join_full/postgresql/go/query.sql.go b/internal/endtoend/testdata/join_full/postgresql/go/query.sql.go new file mode 100644 index 0000000000..fbc977ad37 --- /dev/null +++ b/internal/endtoend/testdata/join_full/postgresql/go/query.sql.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const fullJoin = `-- name: FullJoin :many +SELECT f.id, f.bar_id, b.id +FROM foo f +FULL OUTER JOIN bar b ON b.id = f.bar_id +WHERE f.id = $1 +` + +type FullJoinRow struct { + ID sql.NullInt32 + BarID sql.NullInt32 + ID_2 sql.NullInt32 +} + +func (q *Queries) FullJoin(ctx context.Context, id int32) ([]FullJoinRow, error) { + rows, err := q.db.QueryContext(ctx, fullJoin, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FullJoinRow + for rows.Next() { + var i FullJoinRow + if err := rows.Scan(&i.ID, &i.BarID, &i.ID_2); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/join_full/postgresql/query.sql b/internal/endtoend/testdata/join_full/postgresql/query.sql new file mode 100644 index 0000000000..76abe8a07c --- /dev/null +++ b/internal/endtoend/testdata/join_full/postgresql/query.sql @@ -0,0 +1,8 @@ +CREATE TABLE foo (id serial not null, bar_id int references bar(id)); +CREATE TABLE bar (id serial not null); + +-- name: FullJoin :many +SELECT f.id, f.bar_id, b.id +FROM foo f +FULL OUTER JOIN bar b ON b.id = f.bar_id +WHERE f.id = $1; \ No newline at end of file diff --git a/internal/endtoend/testdata/join_full/postgresql/sqlc.json b/internal/endtoend/testdata/join_full/postgresql/sqlc.json new file mode 100644 index 0000000000..c72b6132d5 --- /dev/null +++ b/internal/endtoend/testdata/join_full/postgresql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/join_left/mysql/go/db.go b/internal/endtoend/testdata/join_left/mysql/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/join_left/mysql/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/join_left/mysql/go/models.go b/internal/endtoend/testdata/join_left/mysql/go/models.go new file mode 100644 index 0000000000..8a39dfa8ef --- /dev/null +++ b/internal/endtoend/testdata/join_left/mysql/go/models.go @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type City struct { + CityID int32 + MayorID int32 +} + +type Mayor struct { + MayorID int32 + FullName string +} + +type User struct { + UserID int32 + CityID sql.NullInt32 +} diff --git a/internal/endtoend/testdata/join_left/mysql/go/query.sql.go b/internal/endtoend/testdata/join_left/mysql/go/query.sql.go new file mode 100644 index 0000000000..c89a48750c --- /dev/null +++ b/internal/endtoend/testdata/join_left/mysql/go/query.sql.go @@ -0,0 +1,83 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const getMayors = `-- name: GetMayors :many +SELECT + user_id, + mayors.full_name +FROM users +LEFT JOIN cities USING (city_id) +INNER JOIN mayors USING (mayor_id) +` + +type GetMayorsRow struct { + UserID int32 + FullName string +} + +func (q *Queries) GetMayors(ctx context.Context) ([]GetMayorsRow, error) { + rows, err := q.db.QueryContext(ctx, getMayors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetMayorsRow + for rows.Next() { + var i GetMayorsRow + if err := rows.Scan(&i.UserID, &i.FullName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getMayorsOptional = `-- name: GetMayorsOptional :many +SELECT + user_id, + mayors.full_name +FROM users +LEFT JOIN cities USING (city_id) +LEFT JOIN mayors USING (mayor_id) +` + +type GetMayorsOptionalRow struct { + UserID int32 + FullName sql.NullString +} + +func (q *Queries) GetMayorsOptional(ctx context.Context) ([]GetMayorsOptionalRow, error) { + rows, err := q.db.QueryContext(ctx, getMayorsOptional) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetMayorsOptionalRow + for rows.Next() { + var i GetMayorsOptionalRow + if err := rows.Scan(&i.UserID, &i.FullName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/join_left/mysql/query.sql b/internal/endtoend/testdata/join_left/mysql/query.sql new file mode 100644 index 0000000000..421f078884 --- /dev/null +++ b/internal/endtoend/testdata/join_left/mysql/query.sql @@ -0,0 +1,29 @@ +--- https://github.com/kyleconroy/sqlc/issues/604 +CREATE TABLE users ( + user_id INT PRIMARY KEY, + city_id INT -- nullable +); +CREATE TABLE cities ( + city_id INT PRIMARY KEY, + mayor_id INT NOT NULL +); +CREATE TABLE mayors ( + mayor_id INT PRIMARY KEY, + full_name TEXT NOT NULL +); + +-- name: GetMayors :many +SELECT + user_id, + mayors.full_name +FROM users +LEFT JOIN cities USING (city_id) +INNER JOIN mayors USING (mayor_id); + +-- name: GetMayorsOptional :many +SELECT + user_id, + mayors.full_name +FROM users +LEFT JOIN cities USING (city_id) +LEFT JOIN mayors USING (mayor_id); \ No newline at end of file diff --git a/internal/endtoend/testdata/join_left/mysql/sqlc.json b/internal/endtoend/testdata/join_left/mysql/sqlc.json new file mode 100644 index 0000000000..c72b6132d5 --- /dev/null +++ b/internal/endtoend/testdata/join_left/mysql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/join_left/postgresql/go/db.go b/internal/endtoend/testdata/join_left/postgresql/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/join_left/postgresql/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/join_left/postgresql/go/models.go b/internal/endtoend/testdata/join_left/postgresql/go/models.go new file mode 100644 index 0000000000..8a39dfa8ef --- /dev/null +++ b/internal/endtoend/testdata/join_left/postgresql/go/models.go @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type City struct { + CityID int32 + MayorID int32 +} + +type Mayor struct { + MayorID int32 + FullName string +} + +type User struct { + UserID int32 + CityID sql.NullInt32 +} diff --git a/internal/endtoend/testdata/join_left/postgresql/go/query.sql.go b/internal/endtoend/testdata/join_left/postgresql/go/query.sql.go new file mode 100644 index 0000000000..c89a48750c --- /dev/null +++ b/internal/endtoend/testdata/join_left/postgresql/go/query.sql.go @@ -0,0 +1,83 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const getMayors = `-- name: GetMayors :many +SELECT + user_id, + mayors.full_name +FROM users +LEFT JOIN cities USING (city_id) +INNER JOIN mayors USING (mayor_id) +` + +type GetMayorsRow struct { + UserID int32 + FullName string +} + +func (q *Queries) GetMayors(ctx context.Context) ([]GetMayorsRow, error) { + rows, err := q.db.QueryContext(ctx, getMayors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetMayorsRow + for rows.Next() { + var i GetMayorsRow + if err := rows.Scan(&i.UserID, &i.FullName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getMayorsOptional = `-- name: GetMayorsOptional :many +SELECT + user_id, + mayors.full_name +FROM users +LEFT JOIN cities USING (city_id) +LEFT JOIN mayors USING (mayor_id) +` + +type GetMayorsOptionalRow struct { + UserID int32 + FullName sql.NullString +} + +func (q *Queries) GetMayorsOptional(ctx context.Context) ([]GetMayorsOptionalRow, error) { + rows, err := q.db.QueryContext(ctx, getMayorsOptional) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetMayorsOptionalRow + for rows.Next() { + var i GetMayorsOptionalRow + if err := rows.Scan(&i.UserID, &i.FullName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/join_left/postgresql/query.sql b/internal/endtoend/testdata/join_left/postgresql/query.sql new file mode 100644 index 0000000000..421f078884 --- /dev/null +++ b/internal/endtoend/testdata/join_left/postgresql/query.sql @@ -0,0 +1,29 @@ +--- https://github.com/kyleconroy/sqlc/issues/604 +CREATE TABLE users ( + user_id INT PRIMARY KEY, + city_id INT -- nullable +); +CREATE TABLE cities ( + city_id INT PRIMARY KEY, + mayor_id INT NOT NULL +); +CREATE TABLE mayors ( + mayor_id INT PRIMARY KEY, + full_name TEXT NOT NULL +); + +-- name: GetMayors :many +SELECT + user_id, + mayors.full_name +FROM users +LEFT JOIN cities USING (city_id) +INNER JOIN mayors USING (mayor_id); + +-- name: GetMayorsOptional :many +SELECT + user_id, + mayors.full_name +FROM users +LEFT JOIN cities USING (city_id) +LEFT JOIN mayors USING (mayor_id); \ No newline at end of file diff --git a/internal/endtoend/testdata/join_left/postgresql/sqlc.json b/internal/endtoend/testdata/join_left/postgresql/sqlc.json new file mode 100644 index 0000000000..c72b6132d5 --- /dev/null +++ b/internal/endtoend/testdata/join_left/postgresql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/join_right/mysql/go/db.go b/internal/endtoend/testdata/join_right/mysql/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/join_right/mysql/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/join_right/mysql/go/models.go b/internal/endtoend/testdata/join_right/mysql/go/models.go new file mode 100644 index 0000000000..d7b3dd5a30 --- /dev/null +++ b/internal/endtoend/testdata/join_right/mysql/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type Bar struct { + ID int32 +} + +type Foo struct { + ID int32 + BarID sql.NullInt32 +} diff --git a/internal/endtoend/testdata/join_right/mysql/go/query.sql.go b/internal/endtoend/testdata/join_right/mysql/go/query.sql.go new file mode 100644 index 0000000000..90c3d09a02 --- /dev/null +++ b/internal/endtoend/testdata/join_right/mysql/go/query.sql.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const rightJoin = `-- name: RightJoin :many +SELECT f.id, f.bar_id, b.id +FROM foo f +RIGHT JOIN bar b ON b.id = f.bar_id +WHERE f.id = $1 +` + +type RightJoinRow struct { + ID sql.NullInt32 + BarID sql.NullInt32 + ID_2 int32 +} + +func (q *Queries) RightJoin(ctx context.Context, id int32) ([]RightJoinRow, error) { + rows, err := q.db.QueryContext(ctx, rightJoin, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []RightJoinRow + for rows.Next() { + var i RightJoinRow + if err := rows.Scan(&i.ID, &i.BarID, &i.ID_2); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/join_right/mysql/query.sql b/internal/endtoend/testdata/join_right/mysql/query.sql new file mode 100644 index 0000000000..f70c29dd05 --- /dev/null +++ b/internal/endtoend/testdata/join_right/mysql/query.sql @@ -0,0 +1,8 @@ +CREATE TABLE foo (id serial not null, bar_id int references bar(id)); +CREATE TABLE bar (id serial not null); + +-- name: RightJoin :many +SELECT f.id, f.bar_id, b.id +FROM foo f +RIGHT JOIN bar b ON b.id = f.bar_id +WHERE f.id = $1; \ No newline at end of file diff --git a/internal/endtoend/testdata/join_right/mysql/sqlc.json b/internal/endtoend/testdata/join_right/mysql/sqlc.json new file mode 100644 index 0000000000..c72b6132d5 --- /dev/null +++ b/internal/endtoend/testdata/join_right/mysql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/join_right/postgresql/go/db.go b/internal/endtoend/testdata/join_right/postgresql/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/join_right/postgresql/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/join_right/postgresql/go/models.go b/internal/endtoend/testdata/join_right/postgresql/go/models.go new file mode 100644 index 0000000000..d7b3dd5a30 --- /dev/null +++ b/internal/endtoend/testdata/join_right/postgresql/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type Bar struct { + ID int32 +} + +type Foo struct { + ID int32 + BarID sql.NullInt32 +} diff --git a/internal/endtoend/testdata/join_right/postgresql/go/query.sql.go b/internal/endtoend/testdata/join_right/postgresql/go/query.sql.go new file mode 100644 index 0000000000..90c3d09a02 --- /dev/null +++ b/internal/endtoend/testdata/join_right/postgresql/go/query.sql.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const rightJoin = `-- name: RightJoin :many +SELECT f.id, f.bar_id, b.id +FROM foo f +RIGHT JOIN bar b ON b.id = f.bar_id +WHERE f.id = $1 +` + +type RightJoinRow struct { + ID sql.NullInt32 + BarID sql.NullInt32 + ID_2 int32 +} + +func (q *Queries) RightJoin(ctx context.Context, id int32) ([]RightJoinRow, error) { + rows, err := q.db.QueryContext(ctx, rightJoin, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []RightJoinRow + for rows.Next() { + var i RightJoinRow + if err := rows.Scan(&i.ID, &i.BarID, &i.ID_2); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/join_right/postgresql/query.sql b/internal/endtoend/testdata/join_right/postgresql/query.sql new file mode 100644 index 0000000000..f70c29dd05 --- /dev/null +++ b/internal/endtoend/testdata/join_right/postgresql/query.sql @@ -0,0 +1,8 @@ +CREATE TABLE foo (id serial not null, bar_id int references bar(id)); +CREATE TABLE bar (id serial not null); + +-- name: RightJoin :many +SELECT f.id, f.bar_id, b.id +FROM foo f +RIGHT JOIN bar b ON b.id = f.bar_id +WHERE f.id = $1; \ No newline at end of file diff --git a/internal/endtoend/testdata/join_right/postgresql/sqlc.json b/internal/endtoend/testdata/join_right/postgresql/sqlc.json new file mode 100644 index 0000000000..c72b6132d5 --- /dev/null +++ b/internal/endtoend/testdata/join_right/postgresql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/params_location/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/params_location/postgresql/stdlib/go/query.sql.go index bef918ed5e..e137611b83 100644 --- a/internal/endtoend/testdata/params_location/postgresql/stdlib/go/query.sql.go +++ b/internal/endtoend/testdata/params_location/postgresql/stdlib/go/query.sql.go @@ -83,8 +83,8 @@ WHERE orders.price > $1 ` type ListUserOrdersRow struct { - ID int32 - FirstName string + ID sql.NullInt32 + FirstName sql.NullString Price string } diff --git a/internal/sql/ast/join_type.go b/internal/sql/ast/join_type.go index 0c77e40875..824e0b357f 100644 --- a/internal/sql/ast/join_type.go +++ b/internal/sql/ast/join_type.go @@ -1,5 +1,19 @@ package ast +// JoinType is the reported type of the join +// Enum copies https://github.com/pganalyze/libpg_query/blob/13-latest/protobuf/pg_query.proto#L2890-L2901 +const ( + _ JoinType = iota + JoinTypeInner + JoinTypeLeft + JoinTypeFull + JoinTypeRight + JoinTypeSemi + JoinTypeAnti + JoinTypeUniqueOuter + JoinTypeUniqueInner +) + type JoinType uint func (n *JoinType) Pos() int {