From 95fc6b769b700979e7189b02e9249542c6dfc4ac Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Tue, 25 Aug 2015 11:39:13 -0500 Subject: [PATCH] v3.0.1 * Add literal bytes and update to c2fo testify [#15](https://github.com/doug-martin/goqu/pull/15) - [@TechnotronicOz](https://github.com/TechnotronicOz) * Refactored escaping of text types to prevent duplication of logic --- HISTORY.md | 4 +++ adapters/mysql/dataset_adapter_test.go | 36 ++++++++++++++++++++ adapters/mysql/mysql.go | 42 +++++++----------------- adapters/sqlite3/dataset_adapter_test.go | 38 ++++++++++++++++++++- adapters/sqlite3/sqlite3.go | 42 +++++++----------------- default_adapter.go | 18 ++++++---- expressions.go | 5 ++- 7 files changed, 114 insertions(+), 71 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index d5bdf605..4d0af85d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,7 @@ +## v3.0.1 + +* Add literal bytes and update to c2fo testify [#15](https://github.com/doug-martin/goqu/pull/15) - [@TechnotronicOz](https://github.com/TechnotronicOz) + ## v3.0.0 * Added support for embedded structs when inserting or updating. [#13](https://github.com/doug-martin/goqu/pull/13) - [@andymoon](https://github.com/andymoon) diff --git a/adapters/mysql/dataset_adapter_test.go b/adapters/mysql/dataset_adapter_test.go index 7fa3852d..b3a5e6df 100644 --- a/adapters/mysql/dataset_adapter_test.go +++ b/adapters/mysql/dataset_adapter_test.go @@ -107,6 +107,42 @@ func (me *datasetAdapterTest) TestLiteralString() { assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\x1atest')") } +func (me *datasetAdapterTest) TestLiteralBytes() { + t := me.T() + ds := me.GetDs("test") + sql, _, err := ds.Where(goqu.I("a").Eq([]byte("test"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test'test"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\'test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte(`test"test`))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\\"test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte(`test\test`))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\\\test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test\ntest"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\ntest')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test\rtest"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\rtest')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test\x00test"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\x00test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test\x1atest"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\x1atest')") +} + func (me *datasetAdapterTest) TestBooleanOperations() { t := me.T() ds := me.GetDs("test") diff --git a/adapters/mysql/mysql.go b/adapters/mysql/mysql.go index c496f686..56034ac2 100644 --- a/adapters/mysql/mysql.go +++ b/adapters/mysql/mysql.go @@ -1,8 +1,6 @@ package mysql -import ( - "gopkg.in/doug-martin/goqu.v3" -) +import "gopkg.in/doug-martin/goqu.v3" var ( placeholder_rune = '?' @@ -32,6 +30,15 @@ var ( goqu.REGEXP_I_LIKE_OP: []byte("REGEXP"), goqu.REGEXP_NOT_I_LIKE_OP: []byte("NOT REGEXP"), } + escape_runes = map[rune][]byte{ + '\'': []byte("\\'"), + '"': []byte("\\\""), + '\\': []byte("\\\\"), + '\n': []byte("\\n"), + '\r': []byte("\\r"), + 0: []byte("\\x00"), + 0x1a: []byte("\\x1a"), + } ) type DatasetAdapter struct { @@ -58,34 +65,6 @@ func (me *DatasetAdapter) SupportsOrderByOnUpdate() bool { return true } -func (me *DatasetAdapter) LiteralString(buf *goqu.SqlBuilder, s string) error { - if buf.IsPrepared { - return me.PlaceHolderSql(buf, s) - } - buf.WriteRune(singlq_quote) - for _, char := range s { - if char == '\'' { // single quote: ' -> \' - buf.WriteString("\\'") - } else if char == '"' { // double quote: " -> \" - buf.WriteString("\\\"") - } else if char == '\\' { // slash: \ -> "\\" - buf.WriteString("\\\\") - } else if char == '\n' { // control: newline: \n -> "\n" - buf.WriteString("\\n") - } else if char == '\r' { // control: return: \r -> "\r" - buf.WriteString("\\r") - } else if char == 0 { // control: NUL: 0 -> "\x00" - buf.WriteString("\\x00") - } else if char == 0x1a { // control: \x1a -> "\x1a" - buf.WriteString("\\x1a") - } else { - buf.WriteRune(char) - } - } - buf.WriteRune(singlq_quote) - return nil -} - func newDatasetAdapter(ds *goqu.Dataset) goqu.Adapter { def := goqu.NewDefaultAdapter(ds).(*goqu.DefaultAdapter) def.PlaceHolderRune = placeholder_rune @@ -96,6 +75,7 @@ func newDatasetAdapter(ds *goqu.Dataset) goqu.Adapter { def.False = mysql_false def.TimeFormat = time_format def.BooleanOperatorLookup = operator_lookup + def.EscapedRunes = escape_runes return &DatasetAdapter{def} } diff --git a/adapters/sqlite3/dataset_adapter_test.go b/adapters/sqlite3/dataset_adapter_test.go index 671e72cd..0477df43 100644 --- a/adapters/sqlite3/dataset_adapter_test.go +++ b/adapters/sqlite3/dataset_adapter_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/c2fo/testify/suite" "github.com/c2fo/testify/assert" + "github.com/c2fo/testify/suite" "gopkg.in/doug-martin/goqu.v3" ) @@ -107,6 +107,42 @@ func (me *datasetAdapterTest) TestLiteralString() { assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\x1atest')") } +func (me *datasetAdapterTest) TestLiteralBytes() { + t := me.T() + ds := me.GetDs("test") + sql, _, err := ds.Where(goqu.I("a").Eq([]byte("test"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test'test"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\'test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte(`test"test`))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\\"test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte(`test\test`))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\\\test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test\ntest"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\ntest')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test\rtest"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\rtest')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test\x00test"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\x00test')") + + sql, _, err = ds.Where(goqu.I("a").Eq([]byte("test\x1atest"))).ToSql() + assert.NoError(t, err) + assert.Equal(t, sql, "SELECT * FROM `test` WHERE (`a` = 'test\\x1atest')") +} + func (me *datasetAdapterTest) TestBooleanOperations() { t := me.T() ds := me.GetDs("test") diff --git a/adapters/sqlite3/sqlite3.go b/adapters/sqlite3/sqlite3.go index 7ea0debe..210f6390 100644 --- a/adapters/sqlite3/sqlite3.go +++ b/adapters/sqlite3/sqlite3.go @@ -1,8 +1,6 @@ package sqlite3 -import ( - "gopkg.in/doug-martin/goqu.v3" -) +import "gopkg.in/doug-martin/goqu.v3" var ( placeholder_rune = '?' @@ -32,6 +30,15 @@ var ( goqu.REGEXP_I_LIKE_OP: []byte("REGEXP"), goqu.REGEXP_NOT_I_LIKE_OP: []byte("NOT REGEXP"), } + escape_runes = map[rune][]byte{ + '\'': []byte("\\'"), + '"': []byte("\\\""), + '\\': []byte("\\\\"), + '\n': []byte("\\n"), + '\r': []byte("\\r"), + 0: []byte("\\x00"), + 0x1a: []byte("\\x1a"), + } ) type DatasetAdapter struct { @@ -58,34 +65,6 @@ func (me *DatasetAdapter) SupportsOrderByOnUpdate() bool { return true } -func (me *DatasetAdapter) LiteralString(buf *goqu.SqlBuilder, s string) error { - if buf.IsPrepared { - return me.PlaceHolderSql(buf, s) - } - buf.WriteRune(singlq_quote) - for _, char := range s { - if char == '\'' { // single quote: ' -> \' - buf.WriteString("\\'") - } else if char == '"' { // double quote: " -> \" - buf.WriteString("\\\"") - } else if char == '\\' { // slash: \ -> "\\" - buf.WriteString("\\\\") - } else if char == '\n' { // control: newline: \n -> "\n" - buf.WriteString("\\n") - } else if char == '\r' { // control: return: \r -> "\r" - buf.WriteString("\\r") - } else if char == 0 { // control: NUL: 0 -> "\x00" - buf.WriteString("\\x00") - } else if char == 0x1a { // control: \x1a -> "\x1a" - buf.WriteString("\\x1a") - } else { - buf.WriteRune(char) - } - } - buf.WriteRune(singlq_quote) - return nil -} - func newDatasetAdapter(ds *goqu.Dataset) goqu.Adapter { def := goqu.NewDefaultAdapter(ds).(*goqu.DefaultAdapter) def.PlaceHolderRune = placeholder_rune @@ -97,6 +76,7 @@ func newDatasetAdapter(ds *goqu.Dataset) goqu.Adapter { def.TimeFormat = time_format def.BooleanOperatorLookup = operator_lookup def.UseLiteralIsBools = false + def.EscapedRunes = escape_runes return &DatasetAdapter{def} } diff --git a/default_adapter.go b/default_adapter.go index 70938532..b0eb1577 100644 --- a/default_adapter.go +++ b/default_adapter.go @@ -90,6 +90,9 @@ var ( NATURAL_FULL_JOIN: []byte(" NATURAL FULL JOIN "), CROSS_JOIN: []byte(" CROSS JOIN "), } + default_escape_runes = map[rune][]byte{ + '\'': []byte("''"), + } ) type ( @@ -129,7 +132,7 @@ type ( WhereFragment []byte //The SQL GROUP BY clause fragment(DEFAULT=[]byte(" GROUP BY ")) GroupByFragment []byte - //The SQL HAVING clause fragment(DEFAULT=[]byte(" HAVING ")) + //The SQL HAVING clause fragment(DELiFAULT=[]byte(" HAVING ")) HavingFragment []byte //The SQL ORDER BY clause fragment(DEFAULT=[]byte(" ORDER BY ")) OrderByFragment []byte @@ -183,6 +186,8 @@ type ( JoinTypeLookup map[JoinType][]byte //Whether or not to use literal TRUE or FALSE for IS statements (e.g. IS TRUE or IS 0) UseLiteralIsBools bool + //EscapedRunes is a map of a rune and the corresponding escape sequence in bytes. Used when escaping text types. + EscapedRunes map[rune][]byte } ) @@ -231,6 +236,7 @@ func NewDefaultAdapter(ds *Dataset) Adapter { JoinTypeLookup: default_join_lookup, TimeFormat: time.RFC3339Nano, UseLiteralIsBools: true, + EscapedRunes: default_escape_runes, } } @@ -626,9 +632,8 @@ func (me *DefaultAdapter) LiteralString(buf *SqlBuilder, s string) error { } buf.WriteRune(me.StringQuote) for _, char := range s { - if char == me.StringQuote { // single quote: ' -> \' - buf.WriteRune(me.StringQuote) - buf.WriteRune(me.StringQuote) + if e, ok := me.EscapedRunes[char]; ok { + buf.Write(e) } else { buf.WriteRune(char) } @@ -647,9 +652,8 @@ func (me *DefaultAdapter) LiteralBytes(buf *SqlBuilder, bs []byte) error { i := 0 for len(bs) > 0 { char, l := utf8.DecodeRune(bs) - if char == me.StringQuote { // single quote: ' -> \' - buf.WriteRune(me.StringQuote) - buf.WriteRune(me.StringQuote) + if e, ok := me.EscapedRunes[char]; ok { + buf.Write(e) } else { buf.WriteRune(char) } diff --git a/expressions.go b/expressions.go index a0439511..1a94114d 100644 --- a/expressions.go +++ b/expressions.go @@ -984,7 +984,10 @@ func checkBoolExpType(op BooleanOperation, lhs Expression, rhs interface{}, inve case reflect.Bool: op = IS_OP case reflect.Slice: - op = IN_OP + //if its a slice of bytes dont treat as an IN + if _, ok := rhs.([]byte); !ok { + op = IN_OP + } case reflect.Struct: switch rhs.(type) { case SqlExpression: