From df5c6a184a6000ea8f80f85cd96c056f365a9342 Mon Sep 17 00:00:00 2001 From: Ryad El-Dajani Date: Fri, 16 Oct 2020 21:20:27 +0200 Subject: [PATCH] Detects nullable fields with LEFT_JOIN, OUTER_JOIN, or RIGHT_JOIN - Builds on top of https://github.com/kyleconroy/sqlc/pull/733 - Properly detects left, right, full joins - Uses proper enum ordering --- examples/booktest/postgresql/db_test.go | 2 +- examples/booktest/postgresql/query.sql.go | 3 +- .../booktest/postgresql/QueriesImpl.kt | 2 +- examples/python/src/booktest/query.py | 2 +- go.mod | 2 +- go.sum | 4 +- internal/compiler/output_columns.go | 48 +++++++++++ .../testdata/join_full/mysql/go/db.go | 29 +++++++ .../testdata/join_full/mysql/go/models.go | 16 ++++ .../testdata/join_full/mysql/go/query.sql.go | 45 ++++++++++ .../testdata/join_full/mysql/query.sql | 8 ++ .../testdata/join_full/mysql/sqlc.json | 12 +++ .../testdata/join_full/postgresql/go/db.go | 29 +++++++ .../join_full/postgresql/go/models.go | 16 ++++ .../join_full/postgresql/go/query.sql.go | 45 ++++++++++ .../testdata/join_full/postgresql/query.sql | 8 ++ .../testdata/join_full/postgresql/sqlc.json | 12 +++ .../testdata/join_left/mysql/go/db.go | 29 +++++++ .../testdata/join_left/mysql/go/models.go | 22 +++++ .../testdata/join_left/mysql/go/query.sql.go | 83 +++++++++++++++++++ .../testdata/join_left/mysql/query.sql | 29 +++++++ .../testdata/join_left/mysql/sqlc.json | 12 +++ .../testdata/join_left/postgresql/go/db.go | 29 +++++++ .../join_left/postgresql/go/models.go | 22 +++++ .../join_left/postgresql/go/query.sql.go | 83 +++++++++++++++++++ .../testdata/join_left/postgresql/query.sql | 29 +++++++ .../testdata/join_left/postgresql/sqlc.json | 12 +++ .../testdata/join_right/mysql/go/db.go | 29 +++++++ .../testdata/join_right/mysql/go/models.go | 16 ++++ .../testdata/join_right/mysql/go/query.sql.go | 45 ++++++++++ .../testdata/join_right/mysql/query.sql | 8 ++ .../testdata/join_right/mysql/sqlc.json | 12 +++ .../testdata/join_right/postgresql/go/db.go | 29 +++++++ .../join_right/postgresql/go/models.go | 16 ++++ .../join_right/postgresql/go/query.sql.go | 45 ++++++++++ .../testdata/join_right/postgresql/query.sql | 8 ++ .../testdata/join_right/postgresql/sqlc.json | 12 +++ .../postgresql/go/query.sql.go | 4 +- internal/sql/ast/join_type.go | 14 ++++ 39 files changed, 862 insertions(+), 9 deletions(-) create mode 100644 internal/endtoend/testdata/join_full/mysql/go/db.go create mode 100644 internal/endtoend/testdata/join_full/mysql/go/models.go create mode 100644 internal/endtoend/testdata/join_full/mysql/go/query.sql.go create mode 100644 internal/endtoend/testdata/join_full/mysql/query.sql create mode 100644 internal/endtoend/testdata/join_full/mysql/sqlc.json create mode 100644 internal/endtoend/testdata/join_full/postgresql/go/db.go create mode 100644 internal/endtoend/testdata/join_full/postgresql/go/models.go create mode 100644 internal/endtoend/testdata/join_full/postgresql/go/query.sql.go create mode 100644 internal/endtoend/testdata/join_full/postgresql/query.sql create mode 100644 internal/endtoend/testdata/join_full/postgresql/sqlc.json create mode 100644 internal/endtoend/testdata/join_left/mysql/go/db.go create mode 100644 internal/endtoend/testdata/join_left/mysql/go/models.go create mode 100644 internal/endtoend/testdata/join_left/mysql/go/query.sql.go create mode 100644 internal/endtoend/testdata/join_left/mysql/query.sql create mode 100644 internal/endtoend/testdata/join_left/mysql/sqlc.json create mode 100644 internal/endtoend/testdata/join_left/postgresql/go/db.go create mode 100644 internal/endtoend/testdata/join_left/postgresql/go/models.go create mode 100644 internal/endtoend/testdata/join_left/postgresql/go/query.sql.go create mode 100644 internal/endtoend/testdata/join_left/postgresql/query.sql create mode 100644 internal/endtoend/testdata/join_left/postgresql/sqlc.json create mode 100644 internal/endtoend/testdata/join_right/mysql/go/db.go create mode 100644 internal/endtoend/testdata/join_right/mysql/go/models.go create mode 100644 internal/endtoend/testdata/join_right/mysql/go/query.sql.go create mode 100644 internal/endtoend/testdata/join_right/mysql/query.sql create mode 100644 internal/endtoend/testdata/join_right/mysql/sqlc.json create mode 100644 internal/endtoend/testdata/join_right/postgresql/go/db.go create mode 100644 internal/endtoend/testdata/join_right/postgresql/go/models.go create mode 100644 internal/endtoend/testdata/join_right/postgresql/go/query.sql.go create mode 100644 internal/endtoend/testdata/join_right/postgresql/query.sql create mode 100644 internal/endtoend/testdata/join_right/postgresql/sqlc.json 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/go.mod b/go.mod index 733b5e51fe..816ddfdfd2 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/jackc/pgx/v4 v4.11.0 github.com/jinzhu/inflection v1.0.0 github.com/kr/pretty v0.2.1 // indirect - github.com/lib/pq v1.10.0 + github.com/lib/pq v1.10.1 github.com/pganalyze/pg_query_go/v2 v2.0.2 github.com/pingcap/parser v0.0.0-20201024025010-3b2fb4b41d73 github.com/spf13/cobra v1.1.3 diff --git a/go.sum b/go.sum index 6f03a7da11..71d9c065ea 100644 --- a/go.sum +++ b/go.sum @@ -259,8 +259,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo= +github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index 0e8329e274..6cf7736032 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 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/go/query.sql.go b/internal/endtoend/testdata/params_location/postgresql/go/query.sql.go index bef918ed5e..e137611b83 100644 --- a/internal/endtoend/testdata/params_location/postgresql/go/query.sql.go +++ b/internal/endtoend/testdata/params_location/postgresql/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 {