Skip to content

Commit b7c9448

Browse files
committed
validateOnConflictColumns
1 parent 4bf2159 commit b7c9448

File tree

4 files changed

+102
-6
lines changed

4 files changed

+102
-6
lines changed

internal/compiler/analyze.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/sqlc-dev/sqlc/internal/sql/ast"
1010
"github.com/sqlc-dev/sqlc/internal/sql/named"
1111
"github.com/sqlc-dev/sqlc/internal/sql/rewrite"
12+
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
1213
"github.com/sqlc-dev/sqlc/internal/sql/validate"
1314
)
1415

@@ -152,6 +153,9 @@ func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, failfast bool)
152153
if err := check(err); err != nil {
153154
return nil, err
154155
}
156+
if err := check(c.validateOnConflictColumns(n)); err != nil {
157+
return nil, err
158+
}
155159
}
156160

157161
if err := check(validate.FuncCall(c.catalog, c.combo, raw)); err != nil {
@@ -213,3 +217,83 @@ func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, failfast bool)
213217
Named: namedParams,
214218
}, rerr
215219
}
220+
221+
// validateOnConflictColumns checks column names in an ON CONFLICT DO UPDATE
222+
// clause against the target table:
223+
// - ON CONFLICT (col, ...) conflict target columns
224+
// - DO UPDATE SET col = ... assignment target columns
225+
// - EXCLUDED.col references in assignment values
226+
func (c *Compiler) validateOnConflictColumns(n *ast.InsertStmt) error {
227+
if n.OnConflictClause == nil || n.OnConflictClause.Action != ast.OnConflictActionUpdate {
228+
return nil
229+
}
230+
fqn, err := ParseTableName(n.Relation)
231+
if err != nil {
232+
return err
233+
}
234+
table, err := c.catalog.GetTable(fqn)
235+
if err != nil {
236+
return err
237+
}
238+
colSet := make(map[string]struct{}, len(table.Columns))
239+
for _, col := range table.Columns {
240+
colSet[col.Name] = struct{}{}
241+
}
242+
243+
// Validate ON CONFLICT (col, ...) conflict target columns.
244+
if n.OnConflictClause.Infer != nil {
245+
for _, item := range n.OnConflictClause.Infer.IndexElems.Items {
246+
elem, ok := item.(*ast.IndexElem)
247+
if !ok || elem.Name == nil {
248+
continue
249+
}
250+
if _, exists := colSet[*elem.Name]; !exists {
251+
e := sqlerr.ColumnNotFound(table.Rel.Name, *elem.Name)
252+
e.Location = n.OnConflictClause.Infer.Location
253+
return e
254+
}
255+
}
256+
}
257+
258+
// Validate DO UPDATE SET col = ... and EXCLUDED.col references.
259+
for _, item := range n.OnConflictClause.TargetList.Items {
260+
target, ok := item.(*ast.ResTarget)
261+
if !ok || target.Name == nil {
262+
continue
263+
}
264+
// Validate the assignment target column.
265+
if _, exists := colSet[*target.Name]; !exists {
266+
e := sqlerr.ColumnNotFound(table.Rel.Name, *target.Name)
267+
e.Location = target.Location
268+
return e
269+
}
270+
// Validate EXCLUDED.col references in the assigned value.
271+
if ref, ok := target.Val.(*ast.ColumnRef); ok {
272+
if col, ok := excludedColumn(ref); ok {
273+
if _, exists := colSet[col]; !exists {
274+
e := sqlerr.ColumnNotFound(table.Rel.Name, col)
275+
e.Location = ref.Location
276+
return e
277+
}
278+
}
279+
}
280+
}
281+
return nil
282+
}
283+
284+
// excludedColumn returns the column name if the ColumnRef is an EXCLUDED.col
285+
// reference, and ok=true. Returns "", false otherwise.
286+
func excludedColumn(ref *ast.ColumnRef) (string, bool) {
287+
if ref.Fields == nil || len(ref.Fields.Items) != 2 {
288+
return "", false
289+
}
290+
first, ok := ref.Fields.Items[0].(*ast.String)
291+
if !ok || first.Str != "excluded" {
292+
return "", false
293+
}
294+
second, ok := ref.Fields.Items[1].(*ast.String)
295+
if !ok {
296+
return "", false
297+
}
298+
return second.Str, true
299+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"contexts": ["managed-db"]
2+
"contexts": ["base"]
33
}
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
-- name: UpsertServer :exec
2-
INSERT INTO servers(code, name) VALUES ($1, $2)
3-
ON CONFLICT (code)
4-
DO UPDATE SET name_typo = 1111;
1+
-- name: UpsertServerSetColumnTypo :exec
2+
INSERT INTO servers(code, name) VALUES ($1, $2)
3+
ON CONFLICT (code)
4+
DO UPDATE SET name_typo = 1111;
5+
6+
-- name: UpsertServerConflictTargetTypo :exec
7+
INSERT INTO servers(code, name) VALUES ($1, $2)
8+
ON CONFLICT (code_typo)
9+
DO UPDATE SET name = 1111;
10+
11+
-- name: UpsertServerExcludedColumnTypo :exec
12+
INSERT INTO servers(code, name) VALUES ($1, $2)
13+
ON CONFLICT (code)
14+
DO UPDATE SET name = EXCLUDED.name_typo;
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
# package querytest
2-
query.sql:4:15: column "name_typo" of relation "servers" does not exist
2+
query.sql:4:15: column "name_typo" of relation "servers" does not exist
3+
query.sql:8:13: column "code_typo" of relation "servers" does not exist
4+
query.sql:14:22: column "name_typo" of relation "servers" does not exist

0 commit comments

Comments
 (0)