From 2933a04f380c45504056ad5d747b5713f40a2781 Mon Sep 17 00:00:00 2001 From: Timothy Studd Date: Thu, 4 Nov 2021 06:51:47 -0700 Subject: [PATCH] fix(compiler): Support nullable fields in joins on same table --- internal/compiler/output_columns.go | 30 ++++++----- internal/compiler/query.go | 7 +-- .../join_left_same_table/mysql/go/db.go | 29 ++++++++++ .../join_left_same_table/mysql/go/models.go | 13 +++++ .../mysql/go/query.sql.go | 54 +++++++++++++++++++ .../join_left_same_table/mysql/query.sql | 15 ++++++ .../join_left_same_table/mysql/sqlc.json | 12 +++++ .../join_left_same_table/postgres/go/db.go | 29 ++++++++++ .../postgres/go/models.go | 13 +++++ .../postgres/go/query.sql.go | 53 ++++++++++++++++++ .../join_left_same_table/postgres/query.sql | 13 +++++ .../join_left_same_table/postgres/sqlc.json | 12 +++++ 12 files changed, 264 insertions(+), 16 deletions(-) create mode 100644 internal/endtoend/testdata/join_left_same_table/mysql/go/db.go create mode 100644 internal/endtoend/testdata/join_left_same_table/mysql/go/models.go create mode 100644 internal/endtoend/testdata/join_left_same_table/mysql/go/query.sql.go create mode 100644 internal/endtoend/testdata/join_left_same_table/mysql/query.sql create mode 100644 internal/endtoend/testdata/join_left_same_table/mysql/sqlc.json create mode 100644 internal/endtoend/testdata/join_left_same_table/postgres/go/db.go create mode 100644 internal/endtoend/testdata/join_left_same_table/postgres/go/models.go create mode 100644 internal/endtoend/testdata/join_left_same_table/postgres/go/query.sql.go create mode 100644 internal/endtoend/testdata/join_left_same_table/postgres/query.sql create mode 100644 internal/endtoend/testdata/join_left_same_table/postgres/sqlc.json diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index 0dbb084549..793e433673 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -261,7 +261,7 @@ func outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, error) { continue } for _, f := range n.FromClause.Items { - if res := isTableRequired(f, col.Table.Name, tableRequired); res != tableNotFound { + if res := isTableRequired(f, col, tableRequired); res != tableNotFound { col.NotNull = res == tableRequired break } @@ -278,18 +278,21 @@ const ( tableOptional ) -func isTableRequired(n ast.Node, tableName string, prior int) int { +func isTableRequired(n ast.Node, col *Column, prior int) int { switch n := n.(type) { case *ast.RangeVar: - if *n.Relname == tableName { + if n.Alias == nil && *n.Relname == col.Table.Name { + return prior + } + if n.Alias != nil && *n.Alias.Aliasname == col.TableAlias && *n.Relname == col.Table.Name { return prior } case *ast.JoinExpr: helper := func(l, r int) int { - if res := isTableRequired(n.Larg, tableName, l); res != tableNotFound { + if res := isTableRequired(n.Larg, col, l); res != tableNotFound { return res } - if res := isTableRequired(n.Rarg, tableName, r); res != tableNotFound { + if res := isTableRequired(n.Rarg, col, r); res != tableNotFound { return res } return tableNotFound @@ -304,7 +307,7 @@ func isTableRequired(n ast.Node, tableName string, prior int) int { } case *ast.List: for _, item := range n.Items { - if res := isTableRequired(item, tableName, prior); res != tableNotFound { + if res := isTableRequired(item, col, prior); res != tableNotFound { return res } } @@ -439,13 +442,14 @@ func outputColumnRefs(res *ast.ResTarget, tables []*Table, node *ast.ColumnRef) cname = *res.Name } cols = append(cols, &Column{ - Name: cname, - Type: c.Type, - Table: c.Table, - DataType: c.DataType, - NotNull: c.NotNull, - IsArray: c.IsArray, - Length: c.Length, + Name: cname, + Type: c.Type, + Table: c.Table, + TableAlias: alias, + DataType: c.DataType, + NotNull: c.NotNull, + IsArray: c.IsArray, + Length: c.Length, }) } } diff --git a/internal/compiler/query.go b/internal/compiler/query.go index c9ef620733..e4721ed0c0 100644 --- a/internal/compiler/query.go +++ b/internal/compiler/query.go @@ -23,9 +23,10 @@ type Column struct { Length *int // XXX: Figure out what PostgreSQL calls `foo.id` - Scope string - Table *ast.TableName - Type *ast.TypeName + Scope string + Table *ast.TableName + TableAlias string + Type *ast.TypeName skipTableRequiredCheck bool } diff --git a/internal/endtoend/testdata/join_left_same_table/mysql/go/db.go b/internal/endtoend/testdata/join_left_same_table/mysql/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/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_same_table/mysql/go/models.go b/internal/endtoend/testdata/join_left_same_table/mysql/go/models.go new file mode 100644 index 0000000000..e73ab6f91a --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/mysql/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type Author struct { + ID int32 + Name string + ParentID sql.NullInt32 +} diff --git a/internal/endtoend/testdata/join_left_same_table/mysql/go/query.sql.go b/internal/endtoend/testdata/join_left_same_table/mysql/go/query.sql.go new file mode 100644 index 0000000000..1ae9c55f41 --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/mysql/go/query.sql.go @@ -0,0 +1,54 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const allAuthors = `-- name: AllAuthors :many +SELECT a.id, + a.name, + p.id as alias_id, + p.name as alias_name +FROM authors a + LEFT JOIN authors p + ON (authors.parent_id = p.id) +` + +type AllAuthorsRow struct { + ID int32 + Name string + AliasID sql.NullInt32 + AliasName sql.NullString +} + +func (q *Queries) AllAuthors(ctx context.Context) ([]AllAuthorsRow, error) { + rows, err := q.db.QueryContext(ctx, allAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AllAuthorsRow + for rows.Next() { + var i AllAuthorsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.AliasID, + &i.AliasName, + ); 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_same_table/mysql/query.sql b/internal/endtoend/testdata/join_left_same_table/mysql/query.sql new file mode 100644 index 0000000000..f62b234fac --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/mysql/query.sql @@ -0,0 +1,15 @@ +CREATE TABLE authors ( + id INT(10) NOT NULL, + name VARCHAR(255) NOT NULL, + parent_id INT(10), + PRIMARY KEY (id) +); + +-- name: AllAuthors :many +SELECT a.id, + a.name, + p.id as alias_id, + p.name as alias_name +FROM authors a + LEFT JOIN authors p + ON (authors.parent_id = p.id); diff --git a/internal/endtoend/testdata/join_left_same_table/mysql/sqlc.json b/internal/endtoend/testdata/join_left_same_table/mysql/sqlc.json new file mode 100644 index 0000000000..534b7e24e9 --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/mysql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "mysql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/join_left_same_table/postgres/go/db.go b/internal/endtoend/testdata/join_left_same_table/postgres/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/postgres/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_same_table/postgres/go/models.go b/internal/endtoend/testdata/join_left_same_table/postgres/go/models.go new file mode 100644 index 0000000000..e73ab6f91a --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/postgres/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type Author struct { + ID int32 + Name string + ParentID sql.NullInt32 +} diff --git a/internal/endtoend/testdata/join_left_same_table/postgres/go/query.sql.go b/internal/endtoend/testdata/join_left_same_table/postgres/go/query.sql.go new file mode 100644 index 0000000000..c8611049df --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/postgres/go/query.sql.go @@ -0,0 +1,53 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const allAuthors = `-- name: AllAuthors :many +SELECT a.id, + a.name, + p.id as alias_id, + p.name as alias_name +FROM authors a + LEFT JOIN authors p USING (parent_id) +` + +type AllAuthorsRow struct { + ID int32 + Name string + AliasID sql.NullInt32 + AliasName sql.NullString +} + +func (q *Queries) AllAuthors(ctx context.Context) ([]AllAuthorsRow, error) { + rows, err := q.db.QueryContext(ctx, allAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AllAuthorsRow + for rows.Next() { + var i AllAuthorsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.AliasID, + &i.AliasName, + ); 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_same_table/postgres/query.sql b/internal/endtoend/testdata/join_left_same_table/postgres/query.sql new file mode 100644 index 0000000000..661b3edfec --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/postgres/query.sql @@ -0,0 +1,13 @@ +CREATE TABLE authors ( + id INT PRIMARY KEY, + name TEXT NOT NULL, + parent_id INT -- nullable +); + +-- name: AllAuthors :many +SELECT a.id, + a.name, + p.id as alias_id, + p.name as alias_name +FROM authors a + LEFT JOIN authors p USING (parent_id); diff --git a/internal/endtoend/testdata/join_left_same_table/postgres/sqlc.json b/internal/endtoend/testdata/join_left_same_table/postgres/sqlc.json new file mode 100644 index 0000000000..af57681f66 --- /dev/null +++ b/internal/endtoend/testdata/join_left_same_table/postgres/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + } + ] +}