Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/howto/insert.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ and use SHOW WARNINGS to check for any problems and roll back if necessary.

Check the [error handling](https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-error-handling) documentation for more information.

Writing the query as `REPLACE INTO` generates `LOAD DATA ... REPLACE INTO TABLE`,
which overwrites rows that collide on a primary or unique key instead of skipping
them.

```sql
CREATE TABLE foo (a text, b integer, c DATETIME, d DATE);

Expand Down
1 change: 1 addition & 0 deletions internal/cmd/shim.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func pluginQueries(r *compiler.Result) []*plugin.Query {
Params: params,
Filename: q.Metadata.Filename,
InsertIntoTable: iit,
InsertIsReplace: q.InsertIsReplace(),
})
}
return out
Expand Down
2 changes: 2 additions & 0 deletions internal/codegen/golang/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ type Query struct {
Arg QueryValue
// Used for :copyfrom
Table *plugin.Identifier
// Used for :copyfrom, true when the query was written as REPLACE INTO
InsertIsReplace bool
}

func (q Query) hasRetType() bool {
Expand Down
17 changes: 9 additions & 8 deletions internal/codegen/golang/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,15 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En
}

gq := Query{
Cmd: query.Cmd,
ConstantName: constantName,
FieldName: sdk.LowerTitle(query.Name) + "Stmt",
MethodName: query.Name,
SourceName: query.Filename,
SQL: query.Text,
Comments: comments,
Table: query.InsertIntoTable,
Cmd: query.Cmd,
ConstantName: constantName,
FieldName: sdk.LowerTitle(query.Name) + "Stmt",
MethodName: query.Name,
SourceName: query.Filename,
SQL: query.Text,
Comments: comments,
Table: query.InsertIntoTable,
InsertIsReplace: query.InsertIsReplace,
}
sqlpkg := parseDriver(options.SqlPackage)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,16 @@ func convertRowsFor{{.MethodName}}(w *io.PipeWriter, {{.Arg.SlicePair}}) {
{{end -}}
// {{.MethodName}} uses MySQL's LOAD DATA LOCAL INFILE and is not atomic.
//
{{if .InsertIsReplace -}}
// Errors are treated as warnings and insertion will continue, even without an
// error for some cases. Rows that collide on a primary or unique key replace
// the existing row. Use this in a transaction and use SHOW WARNINGS to check
// for any problems and roll back if you want to.
{{else -}}
// Errors and duplicate keys are treated as warnings and insertion will
// continue, even without an error for some cases. Use this in a transaction
// and use SHOW WARNINGS to check for any problems and roll back if you want to.
{{end -}}
//
// Check the documentation for more information:
// https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-error-handling
Expand All @@ -40,7 +47,7 @@ func (q *Queries) {{.MethodName}}(ctx context.Context{{if $.EmitMethodsWithDBArg
go convertRowsFor{{.MethodName}}(pw, {{.Arg.Name}})
// The string interpolation is necessary because LOAD DATA INFILE requires
// the file name to be given as a literal string.
result, err := {{if (not $.EmitMethodsWithDBArgument)}}q.{{end}}db.ExecContext(ctx, fmt.Sprintf("LOAD DATA LOCAL INFILE '%s' INTO TABLE {{.TableIdentifierForMySQL}} %s ({{range $index, $name := .Arg.ColumnNames}}{{if gt $index 0}}, {{end}}{{$name}}{{end}})", "Reader::" + rh, mysqltsv.Escaping))
result, err := {{if (not $.EmitMethodsWithDBArgument)}}q.{{end}}db.ExecContext(ctx, fmt.Sprintf("LOAD DATA LOCAL INFILE '%s' {{if .InsertIsReplace}}REPLACE {{end}}INTO TABLE {{.TableIdentifierForMySQL}} %s ({{range $index, $name := .Arg.ColumnNames}}{{if gt $index 0}}, {{end}}{{$name}}{{end}})", "Reader::" + rh, mysqltsv.Escaping))
if err != nil {
return 0, err
}
Expand Down
11 changes: 11 additions & 0 deletions internal/compiler/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ type Query struct {
RawStmt *ast.RawStmt
}

// InsertIsReplace reports whether the query was written as REPLACE INTO. MySQL
// renders a :copyfrom for such a query as LOAD DATA ... REPLACE INTO TABLE, so
// that rows colliding on a key overwrite instead of being skipped.
func (q *Query) InsertIsReplace() bool {
if q.RawStmt == nil {
return false
}
ins, ok := q.RawStmt.Stmt.(*ast.InsertStmt)
return ok && ins.IsReplace
}

type Parameter struct {
Number int
Column *Column
Expand Down
12 changes: 8 additions & 4 deletions internal/endtoend/testdata/codegen_json/gen/codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -66389,7 +66389,8 @@
],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"insert_is_replace": false
},
{
"text": "SELECT id, name, bio FROM authors\nORDER BY name",
Expand Down Expand Up @@ -66478,7 +66479,8 @@
"params": [],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"insert_is_replace": false
},
{
"text": "INSERT INTO authors (\n name, bio\n) VALUES (\n $1, $2\n)\nRETURNING id, name, bio",
Expand Down Expand Up @@ -66630,7 +66632,8 @@
"catalog": "",
"schema": "",
"name": "authors"
}
},
"insert_is_replace": false
},
{
"text": "DELETE FROM authors\nWHERE id = $1",
Expand Down Expand Up @@ -66670,7 +66673,8 @@
],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"insert_is_replace": false
}
],
"sqlc_version": "v1.31.1",
Expand Down
38 changes: 38 additions & 0 deletions internal/endtoend/testdata/copyfrom/mysql/go/copyfrom.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions internal/endtoend/testdata/copyfrom/mysql/go/query.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions internal/endtoend/testdata/copyfrom/mysql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ INSERT INTO foo (a, b, c, d) VALUES (?, ?, ?, ?);

-- name: InsertSingleValue :copyfrom
INSERT INTO foo (a) VALUES (?);

-- name: ReplaceValues :copyfrom
REPLACE INTO foo (a, b, c, d) VALUES (?, ?, ?, ?);
Original file line number Diff line number Diff line change
Expand Up @@ -66391,7 +66391,8 @@
],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"insert_is_replace": false
},
{
"text": "SELECT id, name, bio FROM authors\nORDER BY name",
Expand Down Expand Up @@ -66480,7 +66481,8 @@
"params": [],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"insert_is_replace": false
},
{
"text": "INSERT INTO authors (\n name, bio\n) VALUES (\n $1, $2\n)\nRETURNING id, name, bio",
Expand Down Expand Up @@ -66632,7 +66634,8 @@
"catalog": "",
"schema": "",
"name": "authors"
}
},
"insert_is_replace": false
},
{
"text": "DELETE FROM authors\nWHERE id = $1",
Expand Down Expand Up @@ -66672,7 +66675,8 @@
],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"insert_is_replace": false
}
],
"sqlc_version": "v1.31.1",
Expand Down
1 change: 1 addition & 0 deletions internal/engine/dolphin/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ func (c *cc) convertInsertStmt(n *pcast.InsertStmt) *ast.InsertStmt {
Relation: rangeVar,
Cols: c.convertColumnNames(n.Columns),
ReturningList: &ast.List{},
IsReplace: n.IsReplace,
}
if ss, ok := c.convert(n.Select).(*ast.SelectStmt); ok {
ss.ValuesLists = c.convertLists(n.Lists)
Expand Down
91 changes: 51 additions & 40 deletions internal/plugin/codegen.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion internal/sql/ast/insert_stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type InsertStmt struct {
WithClause *WithClause
Override OverridingKind
DefaultValues bool // SQLite-specific: INSERT INTO ... DEFAULT VALUES
IsReplace bool // MySQL-specific: REPLACE INTO ... instead of INSERT INTO ...
}

func (n *InsertStmt) Pos() int {
Expand All @@ -28,7 +29,11 @@ func (n *InsertStmt) Format(buf *TrackedBuffer, d format.Dialect) {
buf.WriteString(" ")
}

buf.WriteString("INSERT INTO ")
if n.IsReplace {
buf.WriteString("REPLACE INTO ")
} else {
buf.WriteString("INSERT INTO ")
}
if n.Relation != nil {
buf.astFormat(n.Relation, d)
}
Expand Down
1 change: 1 addition & 0 deletions protos/plugin/codegen.proto
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ message Query {
repeated string comments = 6 [json_name = "comments"];
string filename = 7 [json_name = "filename"];
Identifier insert_into_table = 8 [json_name = "insert_into_table"];
bool insert_is_replace = 9 [json_name = "insert_is_replace"];
}

message Parameter {
Expand Down
Loading