diff --git a/enginetest/join_op_tests.go b/enginetest/join_op_tests.go index f2116bca96..cf8c90f67f 100644 --- a/enginetest/join_op_tests.go +++ b/enginetest/join_op_tests.go @@ -2112,6 +2112,47 @@ WHERE }, }, }, + { + // https://github.com/dolthub/dolt/issues/9793 + name: "outer join on false", + setup: [][]string{ + { + "create table t1(c0 int)", + "create table t2(c0 int)", + "insert into t1 values (1)", + "insert into t2 values (1)", + }, + }, + tests: []JoinOpTests{ + { + Query: "select * from t1 full outer join t2 on false", + Expected: []sql.Row{ + {1, nil}, + {nil, 1}, + }, + }, + { + Query: "select * from t1 full outer join t2 on false where t2.c0 is not null and t1.c0 is not null", + Expected: []sql.Row{}, + }, + { + Query: "select * from t1 left outer join t2 on false", + Expected: []sql.Row{{1, nil}}, + }, + { + Query: "select * from t1 left outer join t2 on false where t2.c0 is not null", + Expected: []sql.Row{}, + }, + { + Query: "select * from t1 right outer join t2 on false", + Expected: []sql.Row{{nil, 1}}, + }, + { + Query: "select * from t1 right outer join t2 on false where t1.c0 is not null", + Expected: []sql.Row{}, + }, + }, + }, } var rangeJoinOpTests = []JoinOpTests{ diff --git a/enginetest/queries/script_queries.go b/enginetest/queries/script_queries.go index 958699dc7f..275c914271 100644 --- a/enginetest/queries/script_queries.go +++ b/enginetest/queries/script_queries.go @@ -120,6 +120,175 @@ type ScriptTestAssertion struct { // Unlike other engine tests, ScriptTests must be self-contained. No other tables are created outside the definition of // the tests. var ScriptTests = []ScriptTest{ + { + // https://github.com/dolthub/dolt/issues/9794 + Name: "UPDATE with TRIM function on TEXT column", + SetUpScript: []string{ + "create table my_table (txt text);", + "insert into my_table values('foobar');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "update my_table set txt = trim(txt);", + SkipResultsCheck: true, + }, + { + Query: "select txt from my_table;", + Expected: []sql.Row{{"foobar"}}, + }, + }, + }, + { + // https://github.com/dolthub/dolt/issues/9794 + Name: "String functions with TextStorage (comprehensive test)", + Dialect: "mysql", + SetUpScript: []string{ + "create table test_strings (id int primary key, content text);", + "insert into test_strings values (1, ' Hello World '), (2, 'Test String'), (3, 'LOWERCASE'), (4, 'abc123def');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "select id, trim(content) from test_strings order by id;", + Expected: []sql.Row{{1, "Hello World"}, {2, "Test String"}, {3, "LOWERCASE"}, {4, "abc123def"}}, + }, + { + Query: "select id, upper(content) from test_strings order by id;", + Expected: []sql.Row{{1, " HELLO WORLD "}, {2, "TEST STRING"}, {3, "LOWERCASE"}, {4, "ABC123DEF"}}, + }, + { + Query: "select id, lower(content) from test_strings order by id;", + Expected: []sql.Row{{1, " hello world "}, {2, "test string"}, {3, "lowercase"}, {4, "abc123def"}}, + }, + { + Query: "select id, reverse(content) from test_strings order by id;", + Expected: []sql.Row{{1, " dlroW olleH "}, {2, "gnirtS tseT"}, {3, "ESACREWOL"}, {4, "fed321cba"}}, + }, + { + Query: "select id, substring(content, 1, 5) from test_strings order by id;", + Expected: []sql.Row{{1, " Hel"}, {2, "Test "}, {3, "LOWER"}, {4, "abc12"}}, + }, + { + Query: "select id, length(content) from test_strings order by id;", + Expected: []sql.Row{{1, 15}, {2, 11}, {3, 9}, {4, 9}}, + }, + { + Query: "select id, left(content, 3) from test_strings order by id;", + Expected: []sql.Row{{1, " H"}, {2, "Tes"}, {3, "LOW"}, {4, "abc"}}, + }, + { + Query: "select id, right(content, 3) from test_strings order by id;", + Expected: []sql.Row{{1, "d "}, {2, "ing"}, {3, "ASE"}, {4, "def"}}, + }, + { + Query: "select id, ltrim(content) from test_strings order by id;", + Expected: []sql.Row{{1, "Hello World "}, {2, "Test String"}, {3, "LOWERCASE"}, {4, "abc123def"}}, + }, + { + Query: "select id, rtrim(content) from test_strings order by id;", + Expected: []sql.Row{{1, " Hello World"}, {2, "Test String"}, {3, "LOWERCASE"}, {4, "abc123def"}}, + }, + { + Query: "select id, replace(content, 'e', 'X') from test_strings order by id;", + Expected: []sql.Row{{1, " HXllo World "}, {2, "TXst String"}, {3, "LOWERCASE"}, {4, "abc123dXf"}}, + }, + { + Query: "select id, repeat(substring(content, 1, 2), 2) from test_strings order by id;", + Expected: []sql.Row{{1, " "}, {2, "TeTe"}, {3, "LOLO"}, {4, "abab"}}, + }, + { + Query: "select id, lpad(content, 12, '*') from test_strings where id = 4;", + Expected: []sql.Row{{4, "***abc123def"}}, + }, + { + Query: "select id, rpad(content, 12, '*') from test_strings where id = 4;", + Expected: []sql.Row{{4, "abc123def***"}}, + }, + { + Query: "select id, locate('o', content) from test_strings order by id;", + Expected: []sql.Row{{1, 7}, {2, 0}, {3, 2}, {4, 0}}, + }, + { + Query: "select id, position('o' in content) from test_strings order by id;", + Expected: []sql.Row{{1, 7}, {2, 0}, {3, 2}, {4, 0}}, + }, + { + Query: "select id, substr(content, 2, 4) from test_strings order by id;", + Expected: []sql.Row{{1, " Hel"}, {2, "est "}, {3, "OWER"}, {4, "bc12"}}, + }, + { + Query: "select id, mid(content, 3, 3) from test_strings order by id;", + Expected: []sql.Row{{1, "Hel"}, {2, "st "}, {3, "WER"}, {4, "c12"}}, + }, + { + Query: "select id, char_length(content) from test_strings order by id;", + Expected: []sql.Row{{1, 15}, {2, 11}, {3, 9}, {4, 9}}, + }, + { + Query: "select id, character_length(content) from test_strings order by id;", + Expected: []sql.Row{{1, 15}, {2, 11}, {3, 9}, {4, 9}}, + }, + { + Query: "select id, octet_length(content) from test_strings order by id;", + Expected: []sql.Row{{1, 15}, {2, 11}, {3, 9}, {4, 9}}, + }, + { + Query: "select id, lcase(content) from test_strings order by id;", + Expected: []sql.Row{{1, " hello world "}, {2, "test string"}, {3, "lowercase"}, {4, "abc123def"}}, + }, + { + Query: "select id, ucase(content) from test_strings order by id;", + Expected: []sql.Row{{1, " HELLO WORLD "}, {2, "TEST STRING"}, {3, "LOWERCASE"}, {4, "ABC123DEF"}}, + }, + { + Query: "select id, ascii(content) from test_strings order by id;", + Expected: []sql.Row{{1, uint64(32)}, {2, uint64(84)}, {3, uint64(76)}, {4, uint64(97)}}, + }, + { + Query: "select id, hex(content) from test_strings where id = 4;", + Expected: []sql.Row{{4, "616263313233646566"}}, + }, + { + Query: "select id, unhex(hex(content)) = content from test_strings where id = 4;", + Expected: []sql.Row{{4, true}}, + }, + { + Query: "select id, substring_index(content, 'e', 1) from test_strings order by id;", + Expected: []sql.Row{{1, " H"}, {2, "T"}, {3, "LOWERCASE"}, {4, "abc123d"}}, + }, + { + Query: "select id, insert(content, 2, 3, 'XYZ') from test_strings where id = 4;", + Expected: []sql.Row{{4, "aXYZ23def"}}, + }, + { + Query: "update test_strings set content = concat(trim(content), '!');", + SkipResultsCheck: true, + }, + { + Query: "select id, content from test_strings order by id;", + Expected: []sql.Row{{1, "Hello World!"}, {2, "Test String!"}, {3, "LOWERCASE!"}, {4, "abc123def!"}}, + }, + { + Query: "SELECT CONCAT_WS(',', content, 'suffix') FROM test_strings WHERE id = 2;", + Expected: []sql.Row{{"Test String!,suffix"}}, + }, + { + Query: "SELECT EXPORT_SET(5, content, 'off') FROM test_strings WHERE id = 2;", + Expected: []sql.Row{{"Test String!,off,Test String!,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off,off"}}, + }, + { + Query: "SELECT FIND_IN_SET('String', content) FROM test_strings WHERE id = 2;", + Expected: []sql.Row{{int32(0)}}, + }, + { + Query: "SELECT MAKE_SET(3, content, 'second', 'third') FROM test_strings WHERE id = 2;", + Expected: []sql.Row{{"Test String!,second"}}, + }, + { + Query: "SELECT SOUNDEX(content) FROM test_strings WHERE id = 2;", + Expected: []sql.Row{{"T2323652"}}, + }, + }, + }, { // Regression test for https://github.com/dolthub/dolt/issues/9641 Name: "bit union max1err dolt#9641", diff --git a/sql/analyzer/pushdown.go b/sql/analyzer/pushdown.go index bd24c7bf1a..9595c3e4fe 100644 --- a/sql/analyzer/pushdown.go +++ b/sql/analyzer/pushdown.go @@ -157,11 +157,10 @@ func canDoPushdown(n sql.Node) bool { return true } -// Pushing down a filter is incompatible with the secondary table in a Left -// or Right join. If we push a predicate on the secondary table below the -// join, we end up not evaluating it in all cases (since the secondary table -// result is sometimes null in these types of joins). It must be evaluated -// only after the join result is computed. +// Pushing down a filter is incompatible with the secondary table in a Left or Right join. If we push a predicate on the +// secondary table below the join, we end up not evaluating it in all cases (since the secondary table result is +// sometimes null in these types of joins). It must be evaluated only after the join result is computed. This is also +// true with both tables in a Full Outer join, since either table result could be null. func filterPushdownChildSelector(c transform.Context) bool { switch c.Node.(type) { case *plan.Limit: @@ -178,6 +177,8 @@ func filterPushdownChildSelector(c transform.Context) bool { return false case *plan.JoinNode: switch { + case n.Op.IsFullOuter(): + return false case n.Op.IsMerge(): return false case n.Op.IsLookup(): diff --git a/sql/expression/function/concat_ws.go b/sql/expression/function/concat_ws.go index de6ce27edd..88565409ec 100644 --- a/sql/expression/function/concat_ws.go +++ b/sql/expression/function/concat_ws.go @@ -127,6 +127,12 @@ func (f *ConcatWithSeparator) Eval(ctx *sql.Context, row sql.Row) (interface{}, return nil, err } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + val, err = sql.UnwrapAny(ctx, val) + if err != nil { + return nil, err + } + parts = append(parts, val.(string)) } diff --git a/sql/expression/function/export_set.go b/sql/expression/function/export_set.go index acff3ff7ac..9356ad7b22 100644 --- a/sql/expression/function/export_set.go +++ b/sql/expression/function/export_set.go @@ -171,6 +171,13 @@ func (e *ExportSet) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { if err != nil { return nil, err } + + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + sepStr, err = sql.UnwrapAny(ctx, sepStr) + if err != nil { + return nil, err + } + separatorVal = sepStr.(string) } @@ -206,11 +213,23 @@ func (e *ExportSet) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, err } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + onStr, err = sql.UnwrapAny(ctx, onStr) + if err != nil { + return nil, err + } + offStr, _, err := types.LongText.Convert(ctx, offVal) if err != nil { return nil, err } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + offStr, err = sql.UnwrapAny(ctx, offStr) + if err != nil { + return nil, err + } + bits := bitsInt.(uint64) on := onStr.(string) off := offStr.(string) diff --git a/sql/expression/function/insert.go b/sql/expression/function/insert.go index 55029521bc..97278e58b3 100644 --- a/sql/expression/function/insert.go +++ b/sql/expression/function/insert.go @@ -127,6 +127,12 @@ func (i *Insert) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, err } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + strVal, err = sql.UnwrapAny(ctx, strVal) + if err != nil { + return nil, err + } + posVal, _, err := types.Int64.Convert(ctx, pos) if err != nil { return nil, err @@ -142,6 +148,12 @@ func (i *Insert) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, err } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + newStrVal, err = sql.UnwrapAny(ctx, newStrVal) + if err != nil { + return nil, err + } + s := strVal.(string) p := posVal.(int64) l := lengthVal.(int64) diff --git a/sql/expression/function/locate.go b/sql/expression/function/locate.go index 68bd897ec8..65246f64c9 100644 --- a/sql/expression/function/locate.go +++ b/sql/expression/function/locate.go @@ -104,6 +104,17 @@ func (l *Locate) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, nil } + substrVal, _, err = types.LongText.Convert(ctx, substrVal) + if err != nil { + return nil, err + } + + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + substrVal, err = sql.UnwrapAny(ctx, substrVal) + if err != nil { + return nil, err + } + substr, ok := substrVal.(string) if !ok { return nil, sql.ErrInvalidArgumentDetails.New("locate", "substring must be a string") @@ -118,6 +129,17 @@ func (l *Locate) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, nil } + strVal, _, err = types.LongText.Convert(ctx, strVal) + if err != nil { + return nil, err + } + + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + strVal, err = sql.UnwrapAny(ctx, strVal) + if err != nil { + return nil, err + } + str, ok := strVal.(string) if !ok { return nil, sql.ErrInvalidArgumentDetails.New("locate", "string must be a string") diff --git a/sql/expression/function/make_set.go b/sql/expression/function/make_set.go index 8471706a46..7844128db7 100644 --- a/sql/expression/function/make_set.go +++ b/sql/expression/function/make_set.go @@ -143,6 +143,13 @@ func (m *MakeSet) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { if err != nil { return nil, err } + + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + valStr, err = sql.UnwrapAny(ctx, valStr) + if err != nil { + return nil, err + } + result = append(result, valStr.(string)) } } diff --git a/sql/expression/function/reverse_repeat_replace.go b/sql/expression/function/reverse_repeat_replace.go index 9a6a6d156a..b8287a2763 100644 --- a/sql/expression/function/reverse_repeat_replace.go +++ b/sql/expression/function/reverse_repeat_replace.go @@ -64,6 +64,12 @@ func (r *Reverse) Eval( return nil, err } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + v, err = sql.UnwrapAny(ctx, v) + if err != nil { + return nil, err + } + return reverseString(v.(string)), nil } @@ -162,6 +168,12 @@ func (r *Repeat) Eval( return nil, err } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + str, err = sql.UnwrapAny(ctx, str) + if err != nil { + return nil, err + } + count, err := r.RightChild.Eval(ctx, row) if count == nil || err != nil { return nil, err diff --git a/sql/expression/function/soundex.go b/sql/expression/function/soundex.go index 2744b9a574..1f7b34567c 100644 --- a/sql/expression/function/soundex.go +++ b/sql/expression/function/soundex.go @@ -66,6 +66,12 @@ func (s *Soundex) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, err } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + v, err = sql.UnwrapAny(ctx, v) + if err != nil { + return nil, err + } + var b strings.Builder var last rune for _, c := range strings.ToUpper(v.(string)) { diff --git a/sql/expression/function/substring.go b/sql/expression/function/substring.go index 19a51a46f0..329eca9f5d 100644 --- a/sql/expression/function/substring.go +++ b/sql/expression/function/substring.go @@ -84,6 +84,17 @@ func (s *Substring) Eval( return nil, err } + str, _, err = types.LongText.Convert(ctx, str) + if err != nil { + return nil, err + } + + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + str, err = sql.UnwrapAny(ctx, str) + if err != nil { + return nil, err + } + var text []rune switch str := str.(type) { case string: @@ -223,6 +234,13 @@ func (s *SubstringIndex) Eval(ctx *sql.Context, row sql.Row) (interface{}, error if err != nil { return nil, err } + + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + ex, err = sql.UnwrapAny(ctx, ex) + if err != nil { + return nil, err + } + str, ok := ex.(string) if !ok { return nil, sql.ErrInvalidType.New(reflect.TypeOf(ex).String()) @@ -236,6 +254,13 @@ func (s *SubstringIndex) Eval(ctx *sql.Context, row sql.Row) (interface{}, error if err != nil { return nil, err } + + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + ex, err = sql.UnwrapAny(ctx, ex) + if err != nil { + return nil, err + } + delim, ok := ex.(string) if !ok { return nil, sql.ErrInvalidType.New(reflect.TypeOf(ex).String()) diff --git a/sql/expression/function/trim_ltrim_rtrim.go b/sql/expression/function/trim_ltrim_rtrim.go index b5c6d2a05c..0222b96ded 100644 --- a/sql/expression/function/trim_ltrim_rtrim.go +++ b/sql/expression/function/trim_ltrim_rtrim.go @@ -62,12 +62,18 @@ func (t *Trim) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, err } - // Convert pat into string + // Convert pat into string and unwrap automatically pat, _, err = types.LongText.Convert(ctx, pat) if err != nil { return nil, sql.ErrInvalidType.New(reflect.TypeOf(pat).String()) } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + pat, err = sql.UnwrapAny(ctx, pat) + if err != nil { + return nil, err + } + // Evaluate string value str, err := t.str.Eval(ctx, row) if err != nil { @@ -79,12 +85,18 @@ func (t *Trim) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, nil } - // Convert pat into string + // Convert str to text type and unwrap automatically str, _, err = types.LongText.Convert(ctx, str) if err != nil { return nil, sql.ErrInvalidType.New(reflect.TypeOf(str).String()) } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + str, err = sql.UnwrapAny(ctx, str) + if err != nil { + return nil, err + } + start := 0 end := len(str.(string)) n := len(pat.(string)) @@ -207,6 +219,12 @@ func (t *LeftTrim) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, sql.ErrInvalidType.New(reflect.TypeOf(str)) } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + str, err = sql.UnwrapAny(ctx, str) + if err != nil { + return nil, err + } + return strings.TrimLeftFunc(str.(string), func(r rune) bool { return r == ' ' }), nil @@ -270,6 +288,12 @@ func (t *RightTrim) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, sql.ErrInvalidType.New(reflect.TypeOf(str)) } + // Handle Dolt's TextStorage wrapper that doesn't convert to plain string + str, err = sql.UnwrapAny(ctx, str) + if err != nil { + return nil, err + } + return strings.TrimRightFunc(str.(string), func(r rune) bool { return r == ' ' }), nil