From 2fdf5bfdc5013568143c3b7b8ae6c59d83b644c1 Mon Sep 17 00:00:00 2001 From: Haim Dimer Date: Mon, 17 Aug 2026 00:26:49 -0700 Subject: [PATCH] mysql: keep REPLACE INTO when generating a :copyfrom LOAD DATA A :copyfrom query written as REPLACE INTO generated LOAD DATA LOCAL INFILE '%s' INTO TABLE ..., dropping the REPLACE. MySQL's default for LOAD DATA is to skip rows that collide on a primary or unique key, so the upsert the query asked for silently became a skip. The parser already reports it (marino ast.InsertStmt.IsReplace), but nothing read the bit. Carry it on ast.InsertStmt, derive it from the raw statement in the compiler, add it to plugin.Query, and emit REPLACE INTO TABLE from the template. Fixes #4339 --- docs/howto/insert.md | 4 + internal/cmd/shim.go | 1 + internal/codegen/golang/query.go | 2 + internal/codegen/golang/result.go | 17 ++-- .../go-sql-driver-mysql/copyfromCopy.tmpl | 9 +- internal/compiler/query.go | 11 +++ .../testdata/codegen_json/gen/codegen.json | 12 ++- .../testdata/copyfrom/mysql/go/copyfrom.go | 38 ++++++++ .../testdata/copyfrom/mysql/go/query.sql.go | 11 +++ .../testdata/copyfrom/mysql/query.sql | 3 + .../gen/codegen.json | 12 ++- internal/engine/dolphin/convert.go | 1 + internal/plugin/codegen.pb.go | 91 +++++++++++-------- internal/sql/ast/insert_stmt.go | 7 +- protos/plugin/codegen.proto | 1 + 15 files changed, 162 insertions(+), 58 deletions(-) diff --git a/docs/howto/insert.md b/docs/howto/insert.md index 7bb02d6745..0a2945b509 100644 --- a/docs/howto/insert.md +++ b/docs/howto/insert.md @@ -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); diff --git a/internal/cmd/shim.go b/internal/cmd/shim.go index 654500429a..7ca3d2f14b 100644 --- a/internal/cmd/shim.go +++ b/internal/cmd/shim.go @@ -161,6 +161,7 @@ func pluginQueries(r *compiler.Result) []*plugin.Query { Params: params, Filename: q.Metadata.Filename, InsertIntoTable: iit, + InsertIsReplace: q.InsertIsReplace(), }) } return out diff --git a/internal/codegen/golang/query.go b/internal/codegen/golang/query.go index 27c596c24e..8b6b29908d 100644 --- a/internal/codegen/golang/query.go +++ b/internal/codegen/golang/query.go @@ -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 { diff --git a/internal/codegen/golang/result.go b/internal/codegen/golang/result.go index c5126602da..31c1a5599e 100644 --- a/internal/codegen/golang/result.go +++ b/internal/codegen/golang/result.go @@ -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) diff --git a/internal/codegen/golang/templates/go-sql-driver-mysql/copyfromCopy.tmpl b/internal/codegen/golang/templates/go-sql-driver-mysql/copyfromCopy.tmpl index e21475b148..0aa89f3805 100644 --- a/internal/codegen/golang/templates/go-sql-driver-mysql/copyfromCopy.tmpl +++ b/internal/codegen/golang/templates/go-sql-driver-mysql/copyfromCopy.tmpl @@ -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 @@ -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 } diff --git a/internal/compiler/query.go b/internal/compiler/query.go index b3cf9d6154..08211a989b 100644 --- a/internal/compiler/query.go +++ b/internal/compiler/query.go @@ -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 diff --git a/internal/endtoend/testdata/codegen_json/gen/codegen.json b/internal/endtoend/testdata/codegen_json/gen/codegen.json index 1e3a217541..558daccca4 100644 --- a/internal/endtoend/testdata/codegen_json/gen/codegen.json +++ b/internal/endtoend/testdata/codegen_json/gen/codegen.json @@ -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", @@ -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", @@ -66630,7 +66632,8 @@ "catalog": "", "schema": "", "name": "authors" - } + }, + "insert_is_replace": false }, { "text": "DELETE FROM authors\nWHERE id = $1", @@ -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", diff --git a/internal/endtoend/testdata/copyfrom/mysql/go/copyfrom.go b/internal/endtoend/testdata/copyfrom/mysql/go/copyfrom.go index b5ce436565..292b79fd10 100644 --- a/internal/endtoend/testdata/copyfrom/mysql/go/copyfrom.go +++ b/internal/endtoend/testdata/copyfrom/mysql/go/copyfrom.go @@ -86,3 +86,41 @@ func (q *Queries) InsertValues(ctx context.Context, arg []InsertValuesParams) (i } return result.RowsAffected() } + +var readerHandlerSequenceForReplaceValues uint32 = 1 + +func convertRowsForReplaceValues(w *io.PipeWriter, arg []ReplaceValuesParams) { + e := mysqltsv.NewEncoder(w, 4, nil) + for _, row := range arg { + e.AppendValue(row.A) + e.AppendValue(row.B) + e.AppendValue(row.C) + e.AppendValue(row.D) + } + w.CloseWithError(e.Close()) +} + +// ReplaceValues uses MySQL's LOAD DATA LOCAL INFILE and is not atomic. +// +// 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. +// +// Check the documentation for more information: +// https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-error-handling +func (q *Queries) ReplaceValues(ctx context.Context, arg []ReplaceValuesParams) (int64, error) { + pr, pw := io.Pipe() + defer pr.Close() + rh := fmt.Sprintf("ReplaceValues_%d", atomic.AddUint32(&readerHandlerSequenceForReplaceValues, 1)) + mysql.RegisterReaderHandler(rh, func() io.Reader { return pr }) + defer mysql.DeregisterReaderHandler(rh) + go convertRowsForReplaceValues(pw, arg) + // The string interpolation is necessary because LOAD DATA INFILE requires + // the file name to be given as a literal string. + result, err := q.db.ExecContext(ctx, fmt.Sprintf("LOAD DATA LOCAL INFILE '%s' REPLACE INTO TABLE `foo` %s (a, b, c, d)", "Reader::"+rh, mysqltsv.Escaping)) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/internal/endtoend/testdata/copyfrom/mysql/go/query.sql.go b/internal/endtoend/testdata/copyfrom/mysql/go/query.sql.go index 91bd6628f8..624274fd3c 100644 --- a/internal/endtoend/testdata/copyfrom/mysql/go/query.sql.go +++ b/internal/endtoend/testdata/copyfrom/mysql/go/query.sql.go @@ -23,3 +23,14 @@ type InsertValuesParams struct { C sql.NullTime D sql.NullTime } + +const replaceValues = `-- name: ReplaceValues :copyfrom +REPLACE INTO foo (a, b, c, d) VALUES (?, ?, ?, ?) +` + +type ReplaceValuesParams struct { + A sql.NullString + B sql.NullInt32 + C sql.NullTime + D sql.NullTime +} diff --git a/internal/endtoend/testdata/copyfrom/mysql/query.sql b/internal/endtoend/testdata/copyfrom/mysql/query.sql index 4c5fd5f54c..2a83d11b10 100644 --- a/internal/endtoend/testdata/copyfrom/mysql/query.sql +++ b/internal/endtoend/testdata/copyfrom/mysql/query.sql @@ -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 (?, ?, ?, ?); diff --git a/internal/endtoend/testdata/process_plugin_sqlc_gen_json/gen/codegen.json b/internal/endtoend/testdata/process_plugin_sqlc_gen_json/gen/codegen.json index c4556eebee..db3917b62d 100644 --- a/internal/endtoend/testdata/process_plugin_sqlc_gen_json/gen/codegen.json +++ b/internal/endtoend/testdata/process_plugin_sqlc_gen_json/gen/codegen.json @@ -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", @@ -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", @@ -66632,7 +66634,8 @@ "catalog": "", "schema": "", "name": "authors" - } + }, + "insert_is_replace": false }, { "text": "DELETE FROM authors\nWHERE id = $1", @@ -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", diff --git a/internal/engine/dolphin/convert.go b/internal/engine/dolphin/convert.go index cfd83b5c4d..7efee84793 100644 --- a/internal/engine/dolphin/convert.go +++ b/internal/engine/dolphin/convert.go @@ -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) diff --git a/internal/plugin/codegen.pb.go b/internal/plugin/codegen.pb.go index 525ffc72ef..87d2f8feeb 100644 --- a/internal/plugin/codegen.pb.go +++ b/internal/plugin/codegen.pb.go @@ -816,6 +816,7 @@ type Query struct { Comments []string `protobuf:"bytes,6,rep,name=comments,proto3" json:"comments,omitempty"` Filename string `protobuf:"bytes,7,opt,name=filename,proto3" json:"filename,omitempty"` InsertIntoTable *Identifier `protobuf:"bytes,8,opt,name=insert_into_table,proto3" json:"insert_into_table,omitempty"` + InsertIsReplace bool `protobuf:"varint,9,opt,name=insert_is_replace,proto3" json:"insert_is_replace,omitempty"` } func (x *Query) Reset() { @@ -906,6 +907,13 @@ func (x *Query) GetInsertIntoTable() *Identifier { return nil } +func (x *Query) GetInsertIsReplace() bool { + if x != nil { + return x.InsertIsReplace + } + return false +} + type Parameter struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1308,7 +1316,7 @@ var file_plugin_codegen_proto_rawDesc = []byte{ 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x72, 0x72, 0x61, 0x79, 0x5f, 0x64, 0x69, 0x6d, 0x73, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x72, 0x72, 0x61, 0x79, 0x44, 0x69, 0x6d, 0x73, 0x22, 0x94, + 0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x72, 0x72, 0x61, 0x79, 0x44, 0x69, 0x6d, 0x73, 0x22, 0xc2, 0x02, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, @@ -1326,45 +1334,48 @@ var file_plugin_codegen_proto_rawDesc = []byte{ 0x74, 0x6f, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x11, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x6f, 0x5f, - 0x74, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x4b, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, - 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, - 0x6d, 0x6e, 0x22, 0x87, 0x02, 0x0a, 0x0f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, - 0x69, 0x6e, 0x67, 0x73, 0x12, 0x29, 0x0a, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x43, - 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, - 0x27, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x71, 0x6c, 0x63, - 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x73, 0x71, 0x6c, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x67, 0x6c, - 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x36, 0x0a, 0x10, - 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x22, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x0c, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, - 0x69, 0x6c, 0x65, 0x73, 0x32, 0x4f, 0x0a, 0x0e, 0x43, 0x6f, 0x64, 0x65, 0x67, 0x65, 0x6e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x12, 0x17, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x6e, 0x65, - 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x7c, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x42, 0x0c, 0x43, 0x6f, 0x64, 0x65, 0x67, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x50, 0x01, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x73, 0x71, 0x6c, 0x63, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x73, 0x71, 0x6c, 0x63, 0x2f, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xa2, 0x02, 0x03, - 0x50, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xca, 0x02, 0x06, 0x50, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xe2, 0x02, 0x12, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5c, 0x47, - 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x5f, + 0x69, 0x73, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x11, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x70, 0x6c, + 0x61, 0x63, 0x65, 0x22, 0x4b, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, + 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, + 0x22, 0x87, 0x02, 0x0a, 0x0f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, + 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x12, 0x29, 0x0a, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x43, 0x61, 0x74, + 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x27, 0x0a, + 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, + 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x71, 0x6c, 0x63, 0x5f, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x71, + 0x6c, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x67, 0x6c, 0x6f, 0x62, + 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x36, 0x0a, 0x10, 0x47, 0x65, + 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, + 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, + 0x65, 0x73, 0x32, 0x4f, 0x0a, 0x0e, 0x43, 0x6f, 0x64, 0x65, 0x67, 0x65, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, + 0x12, 0x17, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x7c, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x42, 0x0c, 0x43, 0x6f, 0x64, 0x65, 0x67, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x71, + 0x6c, 0x63, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x73, 0x71, 0x6c, 0x63, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xa2, 0x02, 0x03, 0x50, 0x58, + 0x58, 0xaa, 0x02, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xca, 0x02, 0x06, 0x50, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0xe2, 0x02, 0x12, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/sql/ast/insert_stmt.go b/internal/sql/ast/insert_stmt.go index 4d5c8d1df2..28313fc3bf 100644 --- a/internal/sql/ast/insert_stmt.go +++ b/internal/sql/ast/insert_stmt.go @@ -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 { @@ -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) } diff --git a/protos/plugin/codegen.proto b/protos/plugin/codegen.proto index e6faf19bad..2707f813fe 100644 --- a/protos/plugin/codegen.proto +++ b/protos/plugin/codegen.proto @@ -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 {