diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index 557fb7d575..04f532a403 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -9,6 +9,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/astutils" "github.com/sqlc-dev/sqlc/internal/sql/catalog" "github.com/sqlc-dev/sqlc/internal/sql/lang" + "github.com/sqlc-dev/sqlc/internal/sql/preprocess" "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" ) @@ -53,6 +54,8 @@ func hasStarRef(cf *ast.ColumnRef) bool { // Return an error if column references are ambiguous // Return an error if column references don't exist func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, error) { + var embedErr error + tables, err := c.sourceTables(qc, node) if err != nil { return nil, err @@ -267,6 +270,11 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er // add a column with a reference to an embedded table if embed, ok := qc.embeds.Find(n.Location, res.Location); ok { + // Keep the column and report the error with the rest, so an + // analyzer that discards inference errors still sees the embed. + if err := checkEmbedRequired(node, embed, res.Location); err != nil && embedErr == nil { + embedErr = err + } cols = append(cols, &Column{ Name: embed.Table.Name, EmbedTable: embed.Table, @@ -416,7 +424,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er } } - return cols, nil + return cols, embedErr } const ( @@ -467,6 +475,31 @@ func isTableRequired(n ast.Node, col *Column, prior int) int { return tableNotFound } +// An embedded struct is the model of its table, whose columns may not be +// nullable, so it cannot hold the NULLs the optional side of an outer join +// produces. +func checkEmbedRequired(node ast.Node, embed *preprocess.Embed, location int) error { + n, ok := node.(*ast.SelectStmt) + if !ok { + return nil + } + col := &Column{Table: embed.Table, TableAlias: embed.Param()} + for _, f := range n.FromClause.Items { + res := isTableRequired(f, col, tableRequired) + if res != tableNotFound { + if res == tableOptional { + return &sqlerr.Error{ + Code: "0A000", + Message: fmt.Sprintf("sqlc.embed(%s) on the nullable side of an outer join is not supported", embed.Param()), + Location: location, + } + } + break + } + } + return nil +} + type tableVisitor struct { list ast.List } diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/exec.json b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/exec.json new file mode 100644 index 0000000000..97f81fbc66 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/query.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/query.sql new file mode 100644 index 0000000000..b5bda1cd8b --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/query.sql @@ -0,0 +1,3 @@ +-- name: LeftJoin :many +SELECT sqlc.embed(posts), sqlc.embed(users) FROM posts +LEFT JOIN users ON posts.user_id = users.id; diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/schema.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/schema.sql new file mode 100644 index 0000000000..1b9ac96407 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE users ( + id integer PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE posts ( + id integer PRIMARY KEY, + user_id integer +); diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/sqlc.json b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/sqlc.json new file mode 100644 index 0000000000..f717ca2e66 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/stderr.txt b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/stderr.txt new file mode 100644 index 0000000000..4871db3acf --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/postgresql/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:2:27: sqlc.embed(users) on the nullable side of an outer join is not supported diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/exec.json b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/exec.json new file mode 100644 index 0000000000..97f81fbc66 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/query.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/query.sql new file mode 100644 index 0000000000..d28c7d522b --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/query.sql @@ -0,0 +1,3 @@ +-- name: LeftJoin :many +SELECT sqlc.embed(p), sqlc.embed(u) FROM posts AS p +LEFT JOIN users AS u ON p.user_id = u.id; diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/schema.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/schema.sql new file mode 100644 index 0000000000..1b9ac96407 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE users ( + id integer PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE posts ( + id integer PRIMARY KEY, + user_id integer +); diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/sqlc.json b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/sqlc.json new file mode 100644 index 0000000000..cd66df063b --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "sqlite", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/stderr.txt b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/stderr.txt new file mode 100644 index 0000000000..c9c0993cb0 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_left/sqlite/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:2:23: sqlc.embed(u) on the nullable side of an outer join is not supported diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/go/db.go b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *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/sqlc_embed_outer_join_ok/sqlite/go/models.go b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/go/models.go new file mode 100644 index 0000000000..66d8bf17ad --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/go/models.go @@ -0,0 +1,19 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Post struct { + ID int64 + UserID sql.NullInt64 +} + +type User struct { + ID int64 + Name string +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/go/query.sql.go b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/go/query.sql.go new file mode 100644 index 0000000000..19f0069b0d --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/go/query.sql.go @@ -0,0 +1,148 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const innerJoin = `-- name: InnerJoin :many +SELECT p.id, p.user_id, u.id, u.name FROM posts AS p +INNER JOIN users AS u ON p.user_id = u.id +` + +type InnerJoinRow struct { + Post Post + User User +} + +func (q *Queries) InnerJoin(ctx context.Context) ([]InnerJoinRow, error) { + rows, err := q.db.QueryContext(ctx, innerJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []InnerJoinRow + for rows.Next() { + var i InnerJoinRow + if err := rows.Scan( + &i.Post.ID, + &i.Post.UserID, + &i.User.ID, + &i.User.Name, + ); 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 leftSideOfLeftJoin = `-- name: LeftSideOfLeftJoin :many +SELECT p.id, p.user_id, u.name FROM posts AS p +LEFT JOIN users AS u ON p.user_id = u.id +` + +type LeftSideOfLeftJoinRow struct { + Post Post + Name sql.NullString +} + +func (q *Queries) LeftSideOfLeftJoin(ctx context.Context) ([]LeftSideOfLeftJoinRow, error) { + rows, err := q.db.QueryContext(ctx, leftSideOfLeftJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LeftSideOfLeftJoinRow + for rows.Next() { + var i LeftSideOfLeftJoinRow + if err := rows.Scan(&i.Post.ID, &i.Post.UserID, &i.Name); 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 rightSideOfRightJoin = `-- name: RightSideOfRightJoin :many +SELECT u.id, u.name, p.id FROM posts AS p +RIGHT JOIN users AS u ON p.user_id = u.id +` + +type RightSideOfRightJoinRow struct { + User User + ID sql.NullInt64 +} + +func (q *Queries) RightSideOfRightJoin(ctx context.Context) ([]RightSideOfRightJoinRow, error) { + rows, err := q.db.QueryContext(ctx, rightSideOfRightJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []RightSideOfRightJoinRow + for rows.Next() { + var i RightSideOfRightJoinRow + if err := rows.Scan(&i.User.ID, &i.User.Name, &i.ID); 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 selfJoinRequiredAlias = `-- name: SelfJoinRequiredAlias :many +SELECT a.id, a.name, b.name FROM users AS a +LEFT JOIN users AS b ON a.id = b.id +` + +type SelfJoinRequiredAliasRow struct { + User User + Name sql.NullString +} + +func (q *Queries) SelfJoinRequiredAlias(ctx context.Context) ([]SelfJoinRequiredAliasRow, error) { + rows, err := q.db.QueryContext(ctx, selfJoinRequiredAlias) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SelfJoinRequiredAliasRow + for rows.Next() { + var i SelfJoinRequiredAliasRow + if err := rows.Scan(&i.User.ID, &i.User.Name, &i.Name); 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/sqlc_embed_outer_join_ok/sqlite/query.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/query.sql new file mode 100644 index 0000000000..53cfa81854 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/query.sql @@ -0,0 +1,15 @@ +-- name: LeftSideOfLeftJoin :many +SELECT sqlc.embed(p), u.name FROM posts AS p +LEFT JOIN users AS u ON p.user_id = u.id; + +-- name: RightSideOfRightJoin :many +SELECT sqlc.embed(u), p.id FROM posts AS p +RIGHT JOIN users AS u ON p.user_id = u.id; + +-- name: InnerJoin :many +SELECT sqlc.embed(p), sqlc.embed(u) FROM posts AS p +INNER JOIN users AS u ON p.user_id = u.id; + +-- name: SelfJoinRequiredAlias :many +SELECT sqlc.embed(a), b.name FROM users AS a +LEFT JOIN users AS b ON a.id = b.id; diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/schema.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/schema.sql new file mode 100644 index 0000000000..1b9ac96407 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE users ( + id integer PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE posts ( + id integer PRIMARY KEY, + user_id integer +); diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/sqlc.json b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/sqlc.json new file mode 100644 index 0000000000..cd66df063b --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_ok/sqlite/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "sqlite", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/exec.json b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/exec.json new file mode 100644 index 0000000000..97f81fbc66 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/query.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/query.sql new file mode 100644 index 0000000000..47252eb2fc --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/query.sql @@ -0,0 +1,3 @@ +-- name: RightJoin :many +SELECT sqlc.embed(p), sqlc.embed(u) FROM posts AS p +RIGHT JOIN users AS u ON p.user_id = u.id; diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/schema.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/schema.sql new file mode 100644 index 0000000000..1b9ac96407 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE users ( + id integer PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE posts ( + id integer PRIMARY KEY, + user_id integer +); diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/sqlc.json b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/sqlc.json new file mode 100644 index 0000000000..cd66df063b --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "sqlite", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/stderr.txt b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/stderr.txt new file mode 100644 index 0000000000..787617a074 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_right/sqlite/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:2:8: sqlc.embed(p) on the nullable side of an outer join is not supported diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/exec.json b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/exec.json new file mode 100644 index 0000000000..97f81fbc66 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/query.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/query.sql new file mode 100644 index 0000000000..559ebea2d1 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/query.sql @@ -0,0 +1,3 @@ +-- name: SelfJoin :many +SELECT sqlc.embed(a), sqlc.embed(b) FROM users AS a +LEFT JOIN users AS b ON a.id = b.id; diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/schema.sql b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/schema.sql new file mode 100644 index 0000000000..1b9ac96407 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE users ( + id integer PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE posts ( + id integer PRIMARY KEY, + user_id integer +); diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/sqlc.json b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/sqlc.json new file mode 100644 index 0000000000..cd66df063b --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "sqlite", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/stderr.txt b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/stderr.txt new file mode 100644 index 0000000000..a0b94b1ca2 --- /dev/null +++ b/internal/endtoend/testdata/sqlc_embed_outer_join_self/sqlite/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:2:23: sqlc.embed(b) on the nullable side of an outer join is not supported diff --git a/internal/sql/preprocess/preprocess.go b/internal/sql/preprocess/preprocess.go index ad25bc9b2f..eea45a60e9 100644 --- a/internal/sql/preprocess/preprocess.go +++ b/internal/sql/preprocess/preprocess.go @@ -39,6 +39,11 @@ func (e Embed) Orig() string { return fmt.Sprintf("sqlc.embed(%s)", e.param) } +// Param returns the table name or alias the embed was written with. +func (e Embed) Param() string { + return e.param +} + // EmbedSet is the set of embeds in a single statement. type EmbedSet []*Embed