From 6668164591ed817b600024e36add67fa8ca6374b Mon Sep 17 00:00:00 2001 From: Joe Wilner Date: Thu, 6 May 2021 21:42:10 -0400 Subject: [PATCH 1/6] Detects nullable fields with LEFT_JOIN, OUTER_JOIN, or RIGHT_JOIN (#983) - Builds on top of https://github.com/kyleconroy/sqlc/pull/733 - Properly detects left, right, full joins - Uses proper enum ordering Co-authored-by: Ryad El-Dajani --- 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 +- 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 ++++ 37 files changed, 859 insertions(+), 6 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/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 { From c0b6e9c3777e13c3272ef50d60bccd0e21efbc6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 May 2021 13:01:57 -0700 Subject: [PATCH 2/6] build(deps): bump golang from 1.16.3 to 1.16.4 (#1008) Bumps golang from 1.16.3 to 1.16.4. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d5107f22b4cddc6c2ea05bc1e70632d67397c320 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 May 2021 16:34:24 -0700 Subject: [PATCH 3/6] build(deps): bump docker/login-action from 1 to 1.9.0 (#1014) Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 1.9.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v1...v1.9.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2bebc327be..a2cb19dfa1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -14,7 +14,7 @@ jobs: echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') - uses: actions/checkout@v2 - 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 }} From 7647692d0c0b49dfa130a9610f61c664e4f62214 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 May 2021 16:34:40 -0700 Subject: [PATCH 4/6] build(deps): bump actions/checkout from 2 to 2.3.4 (#1013) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 2.3.4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v2.3.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-kotlin.yml | 2 +- .github/workflows/ci-python.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/equinox.yml | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) 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 a2cb19dfa1..b56a940ed4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,7 +12,7 @@ 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.9.0 with: 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' From 422d12008a44f01b31142808e4e2a114df164f2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 May 2021 16:35:01 -0700 Subject: [PATCH 5/6] build(deps): bump docker/build-push-action from 2 to 2.4.0 (#1010) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2 to 2.4.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v2...v2.4.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b56a940ed4..532ac1a8bc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,7 +18,7 @@ jobs: 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 From ad041d0c500a124cc724361be0688a359227a641 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Sat, 15 May 2021 17:44:34 -0700 Subject: [PATCH 6/6] Skip over unknown functions in source tables (#1017) --- internal/compiler/output_columns.go | 9 +++-- .../testdata/func_return/go/query.sql.go | 35 +++++++++++++++++++ .../endtoend/testdata/func_return/query.sql | 5 +++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index 6cf7736032..56e236cecc 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -299,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, @@ -310,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 { @@ -345,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/go/query.sql.go b/internal/endtoend/testdata/func_return/go/query.sql.go index f40859f8e1..990f335916 100644 --- a/internal/endtoend/testdata/func_return/go/query.sql.go +++ b/internal/endtoend/testdata/func_return/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/func_return/query.sql b/internal/endtoend/testdata/func_return/query.sql index f72450421a..cfea2119d0 100644 --- a/internal/endtoend/testdata/func_return/query.sql +++ b/internal/endtoend/testdata/func_return/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;