From a443ebc0ab0b5eaa5182cc5f50ce943a6b33feae Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Mon, 11 Oct 2021 17:07:22 +0300 Subject: [PATCH 1/4] Implement identifier and literal escaping --- go.mod | 12 ++++++-- go.sum | 16 ++++------ mysql.go | 8 +++-- quote.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ quote_test.go | 61 +++++++++++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 quote.go create mode 100644 quote_test.go diff --git a/go.mod b/go.mod index b9bee1a..2f865a5 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,16 @@ module github.com/go-rel/mysql go 1.17 require ( - github.com/go-rel/rel v0.24.0 - github.com/go-rel/sql v0.1.0 + github.com/go-rel/rel v0.25.1-0.20211011102656-a1b38f01d34a + github.com/go-rel/sql v0.1.1-0.20211011073646-a38034248e90 github.com/go-sql-driver/mysql v1.6.0 github.com/stretchr/testify v1.7.0 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum index 232500f..8d96ba1 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,11 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-rel/rel v0.23.0 h1:xVte6QJhCLGQoTeHeUbIWTVDwllFCVzmJ8511XEnbtg= -github.com/go-rel/rel v0.23.0/go.mod h1:/VBLj1U4ZVb53aB3n22RQwX0V9DvckohseDJQibn2Rs= -github.com/go-rel/rel v0.24.0 h1:DXx8DjUwkkg9EcLfS/m5YGGjdV4fjs7dsrcDVS1Zk0w= -github.com/go-rel/rel v0.24.0/go.mod h1:/VBLj1U4ZVb53aB3n22RQwX0V9DvckohseDJQibn2Rs= -github.com/go-rel/sql v0.1.0 h1:fdkdENIgJzoqUfdbQYtPCl85jcu7Ajj8zh/A5hWS+SI= -github.com/go-rel/sql v0.1.0/go.mod h1:vnvQ9jFzTgFvOl5qcntoFXrs9sas39YcIJG30LFlc50= +github.com/go-rel/rel v0.25.1-0.20211007095335-eec7ac68c920/go.mod h1:/VBLj1U4ZVb53aB3n22RQwX0V9DvckohseDJQibn2Rs= +github.com/go-rel/rel v0.25.1-0.20211011102656-a1b38f01d34a h1:FAd8FrXgy+G/44OwXYOk3A0b4lhdoyKMgF9m3h6pTt4= +github.com/go-rel/rel v0.25.1-0.20211011102656-a1b38f01d34a/go.mod h1:/VBLj1U4ZVb53aB3n22RQwX0V9DvckohseDJQibn2Rs= +github.com/go-rel/sql v0.1.1-0.20211011073646-a38034248e90 h1:638QwWIymw8cizASdq594qlDM729vYEtJobdy8wv2Lo= +github.com/go-rel/sql v0.1.1-0.20211011073646-a38034248e90/go.mod h1:ukXvTr/zbEZLu+lJCXcKpMg1Vp7Ps9cfKD+Us4kJREo= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -18,11 +17,9 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -54,7 +51,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -86,14 +82,12 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/mysql.go b/mysql.go index ecb9caf..dacdf6c 100644 --- a/mysql.go +++ b/mysql.go @@ -27,15 +27,17 @@ import ( // Existing connection needs to be created with `clientFoundRows=true` options for update and delete to works correctly. func New(database *db.DB) rel.Adapter { var ( - bufferFactory = builder.BufferFactory{ArgumentPlaceholder: "?", EscapePrefix: "`", EscapeSuffix: "`"} + bufferFactory = builder.BufferFactory{ArgumentPlaceholder: "?", BoolTrueValue: "true", BoolFalseValue: "false", Quoter: Quote{}, ValueConverter: ValueConvert{}} filterBuilder = builder.Filter{} queryBuilder = builder.Query{BufferFactory: bufferFactory, Filter: filterBuilder} InsertBuilder = builder.Insert{BufferFactory: bufferFactory, InsertDefaultValues: true} insertAllBuilder = builder.InsertAll{BufferFactory: bufferFactory} updateBuilder = builder.Update{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder} deleteBuilder = builder.Delete{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder} - tableBuilder = builder.Table{BufferFactory: bufferFactory, ColumnMapper: sql.ColumnMapper} - indexBuilder = builder.Index{BufferFactory: bufferFactory, DropIndexOnTable: true} + ddlBufferFactory = builder.BufferFactory{InlineValues: true, BoolTrueValue: "true", BoolFalseValue: "true", Quoter: Quote{}, ValueConverter: ValueConvert{}} + ddlQueryBuilder = builder.Query{BufferFactory: ddlBufferFactory, Filter: filterBuilder} + tableBuilder = builder.Table{BufferFactory: ddlBufferFactory} + indexBuilder = builder.Index{BufferFactory: ddlBufferFactory, Query: ddlQueryBuilder, Filter: filterBuilder} ) return &sql.SQL{ diff --git a/quote.go b/quote.go new file mode 100644 index 0000000..1273836 --- /dev/null +++ b/quote.go @@ -0,0 +1,83 @@ +package mysql + +import ( + "database/sql/driver" + "strings" + "time" +) + +// Quote MySQL identifiers and literals. +type Quote struct{} + +func (q Quote) ID(name string) string { + end := strings.IndexRune(name, 0) + if end > -1 { + name = name[:end] + } + return "`" + strings.Replace(name, "`", "``", -1) + "`" +} + +func (q Quote) Value(v interface{}) string { + switch v := v.(type) { + default: + panic("unsupported value") + case string: + rv := []rune(v) + buf := make([]rune, len(rv)*2) + pos := 0 + for i := 0; i < len(rv); i++ { + c := rv[i] + switch c { + case '\x00': + buf[pos] = '\\' + buf[pos+1] = '0' + pos += 2 + case '\n': + buf[pos] = '\\' + buf[pos+1] = 'n' + pos += 2 + case '\r': + buf[pos] = '\\' + buf[pos+1] = 'r' + pos += 2 + case '\x1a': + buf[pos] = '\\' + buf[pos+1] = 'Z' + pos += 2 + case '\'': + buf[pos] = '\\' + buf[pos+1] = '\'' + pos += 2 + case '"': + buf[pos] = '\\' + buf[pos+1] = '"' + pos += 2 + case '\\': + buf[pos] = '\\' + buf[pos+1] = '\\' + pos += 2 + default: + buf[pos] = c + pos++ + } + } + + return "'" + string(buf[:pos]) + "'" + } +} + +// ValueConvert converts values to MySQL literals. +type ValueConvert struct{} + +func (c ValueConvert) ConvertValue(v interface{}) (driver.Value, error) { + v, err := driver.DefaultParameterConverter.ConvertValue(v) + if err != nil { + return nil, err + } + switch v := v.(type) { + default: + return v, nil + case time.Time: + return v.Truncate(time.Microsecond).Format("2006-01-02 15:04:05.999999"), nil + } +} diff --git a/quote_test.go b/quote_test.go new file mode 100644 index 0000000..88921ce --- /dev/null +++ b/quote_test.go @@ -0,0 +1,61 @@ +package mysql + +import ( + "database/sql/driver" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestQuote_ID(t *testing.T) { + quoter := Quote{} + + var cases = []struct { + input string + want string + }{ + {`foo`, "`foo`"}, + {`foo bar baz`, "`foo bar baz`"}, + {"foo`bar", "`foo``bar`"}, + {"foo\x00bar", "`foo`"}, + {"\x00foo", "``"}, + } + + for _, test := range cases { + assert.Equal(t, test.want, quoter.ID(test.input)) + } +} + +func TestQuote_Value(t *testing.T) { + quoter := Quote{} + + var cases = []struct { + input string + want string + }{ + {"foo\x00bar", "'foo\\0bar'"}, + {"foo\nbar", "'foo\\nbar'"}, + {"foo\rbar", "'foo\\rbar'"}, + {"foo\x1abar", "'foo\\Zbar'"}, + {"foo\"bar", "'foo\\\"bar'"}, + {"foo\\bar", "'foo\\\\bar'"}, + {"foo'bar", "'foo\\'bar'"}, + } + + for _, test := range cases { + assert.Equal(t, test.want, quoter.Value(test.input)) + } +} + +type customType int + +func (c customType) Value() (driver.Value, error) { + return int(c), nil +} + +func TestValueConvert_CustomType(t *testing.T) { + valuer := ValueConvert{} + v, err := valuer.ConvertValue(customType(1)) + assert.EqualError(t, err, "non-Value type int returned from Value") + assert.Nil(t, v) +} From 7e2ed7f842f8ace78d80094baf59d54d246afb1b Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Mon, 11 Oct 2021 17:35:03 +0300 Subject: [PATCH 2/4] Fix tests and add testing description --- README.md | 14 ++++++++++++++ mysql.go | 4 ++-- quote.go | 1 + quote_test.go | 15 +++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index be630f9..12d1e66 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,17 @@ func main() { ## Supported Database - MySQL 5 and 8 + +## Testing + +### Start PostgreSQL server in Docker + +```console +docker run -it --rm -p 3307:3306 -e "MARIADB_ROOT_PASSWORD=test" -e "MARIADB_DATABASE=rel_test" mariadb:10 +``` + +### Run tests + +```console +MYSQL_DATABASE="root:test@tcp(localhost:3307)/rel_test" go test ./... +``` diff --git a/mysql.go b/mysql.go index dacdf6c..1428add 100644 --- a/mysql.go +++ b/mysql.go @@ -36,8 +36,8 @@ func New(database *db.DB) rel.Adapter { deleteBuilder = builder.Delete{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder} ddlBufferFactory = builder.BufferFactory{InlineValues: true, BoolTrueValue: "true", BoolFalseValue: "true", Quoter: Quote{}, ValueConverter: ValueConvert{}} ddlQueryBuilder = builder.Query{BufferFactory: ddlBufferFactory, Filter: filterBuilder} - tableBuilder = builder.Table{BufferFactory: ddlBufferFactory} - indexBuilder = builder.Index{BufferFactory: ddlBufferFactory, Query: ddlQueryBuilder, Filter: filterBuilder} + tableBuilder = builder.Table{BufferFactory: ddlBufferFactory, ColumnMapper: sql.ColumnMapper} + indexBuilder = builder.Index{BufferFactory: ddlBufferFactory, Query: ddlQueryBuilder, Filter: filterBuilder, DropIndexOnTable: true} ) return &sql.SQL{ diff --git a/quote.go b/quote.go index 1273836..f7fe7f7 100644 --- a/quote.go +++ b/quote.go @@ -22,6 +22,7 @@ func (q Quote) Value(v interface{}) string { default: panic("unsupported value") case string: + // TODO: Need to check on connection for NO_BACKSLASH_ESCAPES rv := []rune(v) buf := make([]rune, len(rv)*2) pos := 0 diff --git a/quote_test.go b/quote_test.go index 88921ce..8b27206 100644 --- a/quote_test.go +++ b/quote_test.go @@ -3,10 +3,18 @@ package mysql import ( "database/sql/driver" "testing" + "time" "github.com/stretchr/testify/assert" ) +func TestQuote_Panic(t *testing.T) { + quoter := Quote{} + assert.PanicsWithValue(t, "unsupported value", func() { + quoter.Value(1) + }) +} + func TestQuote_ID(t *testing.T) { quoter := Quote{} @@ -59,3 +67,10 @@ func TestValueConvert_CustomType(t *testing.T) { assert.EqualError(t, err, "non-Value type int returned from Value") assert.Nil(t, v) } + +func TestValueConvert_DateTime(t *testing.T) { + valuer := ValueConvert{} + v, err := valuer.ConvertValue(time.Unix(1633934368, 0).UTC()) + assert.NoError(t, err) + assert.Equal(t, "2021-10-11 06:39:28", v) +} From 4940c8010fb0354b3be6bf45a3091106e046d753 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Mon, 11 Oct 2021 19:06:20 +0300 Subject: [PATCH 3/4] Fix typo --- mysql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql.go b/mysql.go index 1428add..7d18008 100644 --- a/mysql.go +++ b/mysql.go @@ -34,7 +34,7 @@ func New(database *db.DB) rel.Adapter { insertAllBuilder = builder.InsertAll{BufferFactory: bufferFactory} updateBuilder = builder.Update{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder} deleteBuilder = builder.Delete{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder} - ddlBufferFactory = builder.BufferFactory{InlineValues: true, BoolTrueValue: "true", BoolFalseValue: "true", Quoter: Quote{}, ValueConverter: ValueConvert{}} + ddlBufferFactory = builder.BufferFactory{InlineValues: true, BoolTrueValue: "true", BoolFalseValue: "false", Quoter: Quote{}, ValueConverter: ValueConvert{}} ddlQueryBuilder = builder.Query{BufferFactory: ddlBufferFactory, Filter: filterBuilder} tableBuilder = builder.Table{BufferFactory: ddlBufferFactory, ColumnMapper: sql.ColumnMapper} indexBuilder = builder.Index{BufferFactory: ddlBufferFactory, Query: ddlQueryBuilder, Filter: filterBuilder, DropIndexOnTable: true} From 760a0d7eb1655d0edc6611694e067980ae7f0a8a Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Tue, 12 Oct 2021 06:44:49 +0300 Subject: [PATCH 4/4] Fix typo --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 12d1e66..299c20c 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,11 @@ func main() { ## Supported Database - MySQL 5 and 8 +- MariaDB 10 ## Testing -### Start PostgreSQL server in Docker +### Start MariaDB server in Docker ```console docker run -it --rm -p 3307:3306 -e "MARIADB_ROOT_PASSWORD=test" -e "MARIADB_DATABASE=rel_test" mariadb:10