From f96a527f5e6e8c4d02e5ae55ccc3a2c99779883e Mon Sep 17 00:00:00 2001 From: Elian Date: Mon, 9 Jun 2025 14:28:32 -0700 Subject: [PATCH 01/11] add oct.go impl --- sql/expression/function/oct.go | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 sql/expression/function/oct.go diff --git a/sql/expression/function/oct.go b/sql/expression/function/oct.go new file mode 100644 index 0000000000..d1883290e2 --- /dev/null +++ b/sql/expression/function/oct.go @@ -0,0 +1,80 @@ +// Copyright 2025 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package function + +import ( + "fmt" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +// Oct function provides a string representation for the octal value of N, where N is a decimal number. +type Oct struct { + n sql.Expression +} + +var _ sql.FunctionExpression = (*Oct)(nil) +var _ sql.CollationCoercible = (*Oct)(nil) + +func NewOct(n sql.Expression) sql.Expression { return &Oct{n} } + +func (o *Oct) FunctionName() string { + return "oct" +} + +func (o *Oct) Description() string { + return "returns a string representation for octal value of N, where N is a decimal number." +} + +func (o *Oct) Type() sql.Type { + return types.LongText +} + +func (o *Oct) IsNullable() bool { + return o.n.IsNullable() +} + +func (o *Oct) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { + // Convert a decimal (base 10) number to octal (base 8) + return NewConv( + o.n, + expression.NewLiteral(10, types.Int64), + expression.NewLiteral(8, types.Int64), + ).Eval(ctx, row) +} + +func (o *Oct) Resolved() bool { + return o.n.Resolved() +} + +func (o *Oct) Children() []sql.Expression { + return []sql.Expression{o.n} +} + +func (o *Oct) WithChildren(children ...sql.Expression) (sql.Expression, error) { + if len(children) != 1 { + return nil, sql.ErrInvalidChildrenNumber.New(o, len(children), 1) + } + return NewOct(children[0]), nil +} + +func (o *Oct) String() string { + return fmt.Sprintf("%s(%s)", o.FunctionName(), o.n) +} + +func (*Oct) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { + return ctx.GetCollation(), 4 // strings with collations +} From 6ea7cd5ed87af4e42cbb1f734a56b56fac05b4a2 Mon Sep 17 00:00:00 2001 From: Elian Date: Mon, 9 Jun 2025 14:36:30 -0700 Subject: [PATCH 02/11] add impl docs --- sql/expression/function/oct.go | 10 ++++++++++ sql/expression/function/oct_test.go | 1 + 2 files changed, 11 insertions(+) create mode 100644 sql/expression/function/oct_test.go diff --git a/sql/expression/function/oct.go b/sql/expression/function/oct.go index d1883290e2..526eace64d 100644 --- a/sql/expression/function/oct.go +++ b/sql/expression/function/oct.go @@ -29,24 +29,30 @@ type Oct struct { var _ sql.FunctionExpression = (*Oct)(nil) var _ sql.CollationCoercible = (*Oct)(nil) +// NewOct returns a new Oct expression. func NewOct(n sql.Expression) sql.Expression { return &Oct{n} } +// FunctionName implements sql.FunctionExpression. func (o *Oct) FunctionName() string { return "oct" } +// Description implements sql.FunctionExpression. func (o *Oct) Description() string { return "returns a string representation for octal value of N, where N is a decimal number." } +// Type implements the Expression interface. func (o *Oct) Type() sql.Type { return types.LongText } +// IsNullable implements the Expression interface. func (o *Oct) IsNullable() bool { return o.n.IsNullable() } +// Eval implements the Expression interface. func (o *Oct) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { // Convert a decimal (base 10) number to octal (base 8) return NewConv( @@ -56,14 +62,17 @@ func (o *Oct) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { ).Eval(ctx, row) } +// Resolved implements the Expression interface. func (o *Oct) Resolved() bool { return o.n.Resolved() } +// Children implements the Expression interface. func (o *Oct) Children() []sql.Expression { return []sql.Expression{o.n} } +// WithChildren implements the Expression interface. func (o *Oct) WithChildren(children ...sql.Expression) (sql.Expression, error) { if len(children) != 1 { return nil, sql.ErrInvalidChildrenNumber.New(o, len(children), 1) @@ -75,6 +84,7 @@ func (o *Oct) String() string { return fmt.Sprintf("%s(%s)", o.FunctionName(), o.n) } +// CollationCoercibility implements the interface sql.CollationCoercible. func (*Oct) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return ctx.GetCollation(), 4 // strings with collations } diff --git a/sql/expression/function/oct_test.go b/sql/expression/function/oct_test.go new file mode 100644 index 0000000000..37a2bd7092 --- /dev/null +++ b/sql/expression/function/oct_test.go @@ -0,0 +1 @@ +package function From c7c84e83d59a9acc7b353e1f3c57653bddf2ae01 Mon Sep 17 00:00:00 2001 From: Elian Date: Mon, 9 Jun 2025 16:11:40 -0700 Subject: [PATCH 03/11] add oct_test.go --- sql/expression/function/oct_test.go | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/sql/expression/function/oct_test.go b/sql/expression/function/oct_test.go index 37a2bd7092..dd5d03c2f7 100644 --- a/sql/expression/function/oct_test.go +++ b/sql/expression/function/oct_test.go @@ -1 +1,79 @@ +// Copyright 2025 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package function + +import ( + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" + "math" + "testing" +) + +type test struct { + name string + nType sql.Type + row sql.Row + expected interface{} +} + +func TestOct(t *testing.T) { + tests := []test{ + // NULL input + {"n is nil", types.Int32, sql.NewRow(nil), nil}, + + // Positive numbers + {"positive small", types.Int32, sql.NewRow(8), "10"}, + {"positive medium", types.Int32, sql.NewRow(64), "100"}, + {"positive large", types.Int32, sql.NewRow(4095), "7777"}, + {"positive huge", types.Int64, sql.NewRow(123456789), "726746425"}, + + // Negative numbers + {"negative small", types.Int32, sql.NewRow(-8), "1777777777777777777770"}, + {"negative medium", types.Int32, sql.NewRow(-64), "1777777777777777777700"}, + {"negative large", types.Int32, sql.NewRow(-4095), "1777777777777777770001"}, + + // Zero + {"zero", types.Int32, sql.NewRow(0), "0"}, + + // String inputs + {"string number", types.LongText, sql.NewRow("15"), "17"}, + {"alpha string", types.LongText, sql.NewRow("abc"), "0"}, + {"mixed string", types.LongText, sql.NewRow("123abc"), "173"}, + + // Edge cases + {"max int32", types.Int32, sql.NewRow(math.MaxInt32), "17777777777"}, + {"min int32", types.Int32, sql.NewRow(math.MinInt32), "1777777777760000000000"}, + {"max int64", types.Int64, sql.NewRow(math.MaxInt64), "777777777777777777777"}, + {"min int64", types.Int64, sql.NewRow(math.MinInt64), "1000000000000000000000"}, + + // Decimal numbers + {"decimal", types.Float64, sql.NewRow(15.5), "17"}, + {"negative decimal", types.Float64, sql.NewRow(-15.5), "1777777777777777777761"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewOct(expression.NewGetField(0, tt.nType, "n", true)) + result, err := f.Eval(sql.NewEmptyContext(), tt.row) + if err != nil { + t.Fatal(err) + } + if result != tt.expected { + t.Errorf("got %v; expected %v", result, tt.expected) + } + }) + } +} From 639f18ceff83215e1595fd167a128fb8a6e06d14 Mon Sep 17 00:00:00 2001 From: Elian Date: Mon, 9 Jun 2025 16:13:04 -0700 Subject: [PATCH 04/11] fix nval empty string out of index err and negative floating points treated being handled as positive ints --- sql/expression/function/conv.go | 60 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/sql/expression/function/conv.go b/sql/expression/function/conv.go index 82dcbb02d0..943d5519a8 100644 --- a/sql/expression/function/conv.go +++ b/sql/expression/function/conv.go @@ -136,62 +136,66 @@ func (c *Conv) WithChildren(children ...sql.Expression) (sql.Expression, error) // This conversion truncates nVal as its first subpart that is convertable. // nVal is treated as unsigned except nVal is negative. func convertFromBase(ctx *sql.Context, nVal string, fromBase interface{}) interface{} { - fromBase, _, err := types.Int64.Convert(ctx, fromBase) - if err != nil { + if len(nVal) == 0 { return nil } - fromVal := int(math.Abs(float64(fromBase.(int64)))) + // Convert and validate fromBase + baseVal, _, err := types.Int64.Convert(ctx, fromBase) + if err != nil { + return nil + } + fromVal := int(math.Abs(float64(baseVal.(int64)))) if fromVal < 2 || fromVal > 36 { return nil } + // Handle sign negative := false - var upper string - var lower string - if nVal[0] == '-' { + switch { + case nVal[0] == '-': + if len(nVal) == 1 { + return uint64(0) + } negative = true nVal = nVal[1:] - } else if nVal[0] == '+' { + case nVal[0] == '+': + if len(nVal) == 1 { + return uint64(0) + } nVal = nVal[1:] } - // check for upper and lower bound for given fromBase + // Determine bounds based on sign + var maxLen int if negative { - upper = strconv.FormatInt(math.MaxInt64, fromVal) - lower = strconv.FormatInt(math.MinInt64, fromVal) - if len(nVal) > len(lower) { - nVal = lower - } else if len(nVal) > len(upper) { - nVal = upper + maxLen = len(strconv.FormatInt(math.MinInt64, fromVal)) + if len(nVal) > maxLen { + // Use MinInt64 representation in the given base + nVal = strconv.FormatInt(math.MinInt64, fromVal)[1:] // remove minus sign } } else { - upper = strconv.FormatUint(math.MaxUint64, fromVal) - lower = "0" - if len(nVal) < len(lower) { - nVal = lower - } else if len(nVal) > len(upper) { - nVal = upper + maxLen = len(strconv.FormatUint(math.MaxUint64, fromVal)) + if len(nVal) > maxLen { + // Use MaxUint64 representation in the given base + nVal = strconv.FormatUint(math.MaxUint64, fromVal) } } - truncate := false - result := uint64(0) - i := 1 - for !truncate && i <= len(nVal) { + // Find the longest valid prefix that can be converted + var result uint64 + for i := 1; i <= len(nVal); i++ { val, err := strconv.ParseUint(nVal[:i], fromVal, 64) if err != nil { - truncate = true - return result + break } result = val - i++ } if negative { + // MySQL returns signed value for negative inputs return int64(result) * -1 } - return result } From a873c871f921b4cd85d4851ae401912d74d786bc Mon Sep 17 00:00:00 2001 From: Elian Date: Mon, 9 Jun 2025 16:13:32 -0700 Subject: [PATCH 05/11] add empty string tests --- sql/expression/function/conv_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sql/expression/function/conv_test.go b/sql/expression/function/conv_test.go index 664701be0d..05b00adb38 100644 --- a/sql/expression/function/conv_test.go +++ b/sql/expression/function/conv_test.go @@ -35,6 +35,8 @@ func TestConv(t *testing.T) { {"n is nil", types.Int32, sql.NewRow(nil, 16, 2), nil}, {"fromBase is nil", types.LongText, sql.NewRow('a', nil, 2), nil}, {"toBase is nil", types.LongText, sql.NewRow('a', 16, nil), nil}, + {"empty n string", types.LongText, sql.NewRow("", 3, 4), nil}, + {"empty arg strings", types.LongText, sql.NewRow(4, "", ""), nil}, // invalid inputs {"invalid N", types.LongText, sql.NewRow("r", 16, 2), "0"}, From e68d5e3c6c09e5253a2e4f90d22768e94e06e2b1 Mon Sep 17 00:00:00 2001 From: Elian Date: Mon, 9 Jun 2025 16:30:54 -0700 Subject: [PATCH 06/11] add oct to registry --- sql/expression/function/oct.go | 4 ++-- sql/expression/function/registry.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sql/expression/function/oct.go b/sql/expression/function/oct.go index 526eace64d..219b86b707 100644 --- a/sql/expression/function/oct.go +++ b/sql/expression/function/oct.go @@ -21,7 +21,7 @@ import ( "github.com/dolthub/go-mysql-server/sql/types" ) -// Oct function provides a string representation for the octal value of N, where N is a decimal number. +// Oct function provides a string representation for the octal value of N, where N is a decimal (base 10) number. type Oct struct { n sql.Expression } @@ -39,7 +39,7 @@ func (o *Oct) FunctionName() string { // Description implements sql.FunctionExpression. func (o *Oct) Description() string { - return "returns a string representation for octal value of N, where N is a decimal number." + return "returns a string representation for octal value of N, where N is a decimal (base 10) number." } // Type implements the Expression interface. diff --git a/sql/expression/function/registry.go b/sql/expression/function/registry.go index a6bccbc828..996e855afc 100644 --- a/sql/expression/function/registry.go +++ b/sql/expression/function/registry.go @@ -184,6 +184,7 @@ var BuiltIns = []sql.Function{ sql.Function1{Name: "ntile", Fn: window.NewNTile}, sql.FunctionN{Name: "now", Fn: NewNow}, sql.Function2{Name: "nullif", Fn: NewNullIf}, + sql.Function1{Name: "oct", Fn: NewOct}, sql.Function1{Name: "octet_length", Fn: NewLength}, sql.Function1{Name: "ord", Fn: NewOrd}, sql.Function0{Name: "pi", Fn: NewPi}, From 4ee9999c7384569aeed2b7e7c05b2636e055e18a Mon Sep 17 00:00:00 2001 From: elianddb Date: Mon, 9 Jun 2025 23:47:47 +0000 Subject: [PATCH 07/11] [ga-format-pr] Run ./format_repo.sh to fix formatting --- sql/expression/function/oct.go | 1 + sql/expression/function/oct_test.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sql/expression/function/oct.go b/sql/expression/function/oct.go index 219b86b707..f287de6281 100644 --- a/sql/expression/function/oct.go +++ b/sql/expression/function/oct.go @@ -16,6 +16,7 @@ package function import ( "fmt" + "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" diff --git a/sql/expression/function/oct_test.go b/sql/expression/function/oct_test.go index dd5d03c2f7..7cd978405e 100644 --- a/sql/expression/function/oct_test.go +++ b/sql/expression/function/oct_test.go @@ -15,11 +15,12 @@ package function import ( + "math" + "testing" + "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" - "math" - "testing" ) type test struct { From af645db94118187b3c1da9554e375f6b8f84878e Mon Sep 17 00:00:00 2001 From: Elian Date: Tue, 10 Jun 2025 16:58:19 -0700 Subject: [PATCH 08/11] update switch sql/expression/function/conv.go Co-authored-by: James Cor --- sql/expression/function/conv.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/expression/function/conv.go b/sql/expression/function/conv.go index 943d5519a8..bf80f15d8c 100644 --- a/sql/expression/function/conv.go +++ b/sql/expression/function/conv.go @@ -152,8 +152,8 @@ func convertFromBase(ctx *sql.Context, nVal string, fromBase interface{}) interf // Handle sign negative := false - switch { - case nVal[0] == '-': + switch case nVal[0] { + case '-': if len(nVal) == 1 { return uint64(0) } From 3675fc85d32ee53e71e8eca05b3a21d316064d40 Mon Sep 17 00:00:00 2001 From: Elian Date: Tue, 10 Jun 2025 17:08:39 -0700 Subject: [PATCH 09/11] fix switch syntax --- sql/expression/function/conv.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/expression/function/conv.go b/sql/expression/function/conv.go index bf80f15d8c..517490df1c 100644 --- a/sql/expression/function/conv.go +++ b/sql/expression/function/conv.go @@ -152,14 +152,14 @@ func convertFromBase(ctx *sql.Context, nVal string, fromBase interface{}) interf // Handle sign negative := false - switch case nVal[0] { + switch nVal[0] { case '-': if len(nVal) == 1 { return uint64(0) } negative = true nVal = nVal[1:] - case nVal[0] == '+': + case '+': if len(nVal) == 1 { return uint64(0) } From 96c2e97ece6d752c5cfad274f0b0b0c601735e55 Mon Sep 17 00:00:00 2001 From: Elian Date: Tue, 10 Jun 2025 17:43:45 -0700 Subject: [PATCH 10/11] add query tests --- enginetest/queries/queries.go | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/enginetest/queries/queries.go b/enginetest/queries/queries.go index 9e350bbeee..333185aa9c 100644 --- a/enginetest/queries/queries.go +++ b/enginetest/queries/queries.go @@ -8387,6 +8387,58 @@ SELECT * FROM cte WHERE d = 2;`, Query: "SELECT CONV(i, 10, 2) FROM mytable", Expected: []sql.Row{{"1"}, {"10"}, {"11"}}, }, + { + Query: "SELECT OCT(8)", + Expected: []sql.Row{{"10"}}, + }, + { + Query: "SELECT OCT(255)", + Expected: []sql.Row{{"377"}}, + }, + { + Query: "SELECT OCT(0)", + Expected: []sql.Row{{"0"}}, + }, + { + Query: "SELECT OCT(1)", + Expected: []sql.Row{{"1"}}, + }, + { + Query: "SELECT OCT(NULL)", + Expected: []sql.Row{{nil}}, + }, + { + Query: "SELECT OCT(-1)", + Expected: []sql.Row{{"1777777777777777777777"}}, + }, + { + Query: "SELECT OCT(-8)", + Expected: []sql.Row{{"1777777777777777777770"}}, + }, + { + Query: "SELECT OCT(OCT(4))", + Expected: []sql.Row{{"4"}}, + }, + { + Query: "SELECT OCT('16')", + Expected: []sql.Row{{"20"}}, + }, + { + Query: "SELECT OCT('abc')", + Expected: []sql.Row{{"0"}}, + }, + { + Query: "SELECT OCT(15.7)", + Expected: []sql.Row{{"17"}}, + }, + { + Query: "SELECT OCT(-15.2)", + Expected: []sql.Row{{"1777777777777777777761"}}, + }, + { + Query: "SELECT OCT(HEX(SUBSTRING('127.0', 1, 3)))", + Expected: []sql.Row{{"1143625"}}, + }, { Query: `SELECT t1.pk from one_pk join (one_pk t1 join one_pk t2 on t1.pk = t2.pk) on t1.pk = one_pk.pk and one_pk.pk = 1 join (one_pk t3 join one_pk t4 on t3.c1 is not null) on t3.pk = one_pk.pk and one_pk.c1 = 10`, Expected: []sql.Row{{1}, {1}, {1}, {1}}, From 9cf0f6d7b2f6fe623243ab1f825c39785628ec55 Mon Sep 17 00:00:00 2001 From: Elian Date: Wed, 11 Jun 2025 14:03:52 -0700 Subject: [PATCH 11/11] add table queries using oct() --- enginetest/queries/queries.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/enginetest/queries/queries.go b/enginetest/queries/queries.go index 333185aa9c..75b41b482b 100644 --- a/enginetest/queries/queries.go +++ b/enginetest/queries/queries.go @@ -8439,6 +8439,26 @@ SELECT * FROM cte WHERE d = 2;`, Query: "SELECT OCT(HEX(SUBSTRING('127.0', 1, 3)))", Expected: []sql.Row{{"1143625"}}, }, + { + Query: "SELECT i, OCT(i), OCT(-i), OCT(i * 2) FROM mytable ORDER BY i", + Expected: []sql.Row{ + {1, "1", "1777777777777777777777", "2"}, + {2, "2", "1777777777777777777776", "4"}, + {3, "3", "1777777777777777777775", "6"}, + }, + }, + { + Query: "SELECT OCT(i) FROM mytable ORDER BY CONV(i, 10, 16)", + Expected: []sql.Row{{"1"}, {"2"}, {"3"}}, + }, + { + Query: "SELECT i FROM mytable WHERE OCT(s) > 0", + Expected: []sql.Row{}, + }, + { + Query: "SELECT s FROM mytable WHERE OCT(i*123) < 400", + Expected: []sql.Row{{"first row"}, {"second row"}}, + }, { Query: `SELECT t1.pk from one_pk join (one_pk t1 join one_pk t2 on t1.pk = t2.pk) on t1.pk = one_pk.pk and one_pk.pk = 1 join (one_pk t3 join one_pk t4 on t3.c1 is not null) on t3.pk = one_pk.pk and one_pk.c1 = 10`, Expected: []sql.Row{{1}, {1}, {1}, {1}},