diff --git a/server/ast/expr.go b/server/ast/expr.go index 8b2c95a1af..23a9643144 100644 --- a/server/ast/expr.go +++ b/server/ast/expr.go @@ -476,7 +476,12 @@ func nodeExpr(ctx *Context, node tree.Expr) (vitess.Expr, error) { case *tree.DArray: return nil, errors.Errorf("the statement is not yet supported") case *tree.DBitArray: - return nil, errors.Errorf("the statement is not yet supported") + // We convert bitarray to string representation for engine representation purposes so that we don't have to + // represent another fundamental golang type. This means our representation in memory is more verbose. + bitStr := tree.AsStringWithFlags(node, tree.FmtPgwireText) + return vitess.InjectedExpr{ + Expression: pgexprs.NewUnsafeLiteral(bitStr, pgtypes.Bit), + }, nil case *tree.DBool: return vitess.InjectedExpr{ Expression: pgexprs.NewRawLiteralBool(bool(*node)), diff --git a/server/ast/resolvable_type_reference.go b/server/ast/resolvable_type_reference.go index a82dc32586..599e908054 100755 --- a/server/ast/resolvable_type_reference.go +++ b/server/ast/resolvable_type_reference.go @@ -158,12 +158,38 @@ func nodeResolvableTypeReference(ctx *Context, typ tree.ResolvableTypeReference) resolvedType = pgtypes.VarChar } else { resolvedType, err = pgtypes.NewVarCharType(int32(width)) - } - if err != nil { - return nil, nil, err + if err != nil { + return nil, nil, err + } } case oid.T_xid: resolvedType = pgtypes.Xid + case oid.T_bit: + width := uint32(columnType.Width()) + if width > pgtypes.StringMaxLength { + return nil, nil, errors.Errorf("length for type bit cannot exceed %d", pgtypes.StringMaxLength) + } else if width == 0 { + // TODO: need to differentiate between definitions 'bit' (valid) and 'bit(0)' (invalid) + resolvedType = pgtypes.Bit + } else { + resolvedType, err = pgtypes.NewBitType(int32(width)) + if err != nil { + return nil, nil, err + } + } + case oid.T_varbit: + width := uint32(columnType.Width()) + if width > pgtypes.StringMaxLength { + return nil, nil, errors.Errorf("length for type varbit cannot exceed %d", pgtypes.StringMaxLength) + } else if width == 0 { + // TODO: need to differentiate between definitions 'varbit' (valid) and 'varbit(0)' (invalid) + resolvedType = pgtypes.VarBit + } else { + resolvedType, err = pgtypes.NewVarBitType(int32(width)) + if err != nil { + return nil, nil, err + } + } default: return nil, nil, errors.Errorf("unknown type with oid: %d", uint32(columnType.Oid())) } diff --git a/server/cast/bit.go b/server/cast/bit.go new file mode 100644 index 0000000000..173a5ca580 --- /dev/null +++ b/server/cast/bit.go @@ -0,0 +1,102 @@ +// Copyright 2026 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 cast + +import ( + "github.com/cockroachdb/errors" + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/postgres/parser/sem/tree" + + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initBit handles all casts that are built-in. This comprises only the "From" types. +func initBit() { + bitExplicit() + bitImplicit() +} + +// bitExplicit registers all explicit casts. This comprises only the "From" types. +func bitExplicit() { + framework.MustAddExplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.Bit, + ToType: pgtypes.Int32, + Function: func(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) { + array, err := tree.ParseDBitArray(val.(string)) + if err != nil { + return nil, err + } + if array.BitLen() > 32 { + return nil, errors.Wrap(pgtypes.ErrCastOutOfRange, "integer out of range") + } + return int32(array.AsInt64(32)), nil + }, + }) + framework.MustAddExplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.Bit, + ToType: pgtypes.Int64, + Function: func(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) { + array, err := tree.ParseDBitArray(val.(string)) + if err != nil { + return nil, err + } + if array.BitLen() > 64 { + return nil, errors.Wrap(pgtypes.ErrCastOutOfRange, "bigint out of range") + } + return array.AsInt64(64), nil + }, + }) +} + +// bitImplicit registers all implicit casts. This comprises only the "From" types. +func bitImplicit() { + framework.MustAddImplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.Bit, + ToType: pgtypes.Bit, + Function: func(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) { + input := val.(string) + array, err := tree.ParseDBitArray(input) + if err != nil { + return nil, err + } + expectedLength := pgtypes.GetCharLengthFromTypmod(targetType.GetAttTypMod()) + if array.BitLen() != uint(expectedLength) { + return nil, pgtypes.ErrWrongLengthBit.New(len(input), expectedLength) + } + return tree.AsStringWithFlags(array, tree.FmtPgwireText), nil + }, + }) + framework.MustAddImplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.Bit, + ToType: pgtypes.VarBit, + Function: func(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) { + input := val.(string) + array, err := tree.ParseDBitArray(input) + if err != nil { + return nil, err + } + atttypmod := targetType.GetAttTypMod() + if atttypmod != -1 { + maxLength := pgtypes.GetCharLengthFromTypmod(atttypmod) + if int32(array.BitLen()) > maxLength { + return nil, pgtypes.ErrVarBitLengthExceeded.New(maxLength) + } + } + return tree.AsStringWithFlags(array, tree.FmtPgwireText), nil + }, + }) +} diff --git a/server/cast/init.go b/server/cast/init.go index 9c80e3ad6a..47f5b16efa 100644 --- a/server/cast/init.go +++ b/server/cast/init.go @@ -21,6 +21,7 @@ import ( // Init initializes all casts in this package. func Init() { + initBit() initBool() initChar() initDate() @@ -44,6 +45,7 @@ func Init() { initTimestamp() initTimestampTZ() initTimeTZ() + initVarBit() initVarChar() // This is a hack to get around import cycles. The types package needs these references for type conversions in diff --git a/server/cast/varbit.go b/server/cast/varbit.go new file mode 100644 index 0000000000..0c7a920654 --- /dev/null +++ b/server/cast/varbit.go @@ -0,0 +1,68 @@ +// Copyright 2026 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 cast + +import ( + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/postgres/parser/sem/tree" + + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initVarBit handles all casts that are built-in. This comprises only the "From" types. +func initVarBit() { + varBitImplicit() +} + +// varBitImplicit registers all implicit casts. This comprises only the "From" types. +func varBitImplicit() { + framework.MustAddImplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.VarBit, + ToType: pgtypes.Bit, + Function: func(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) { + input := val.(string) + array, err := tree.ParseDBitArray(input) + if err != nil { + return nil, err + } + expectedLength := pgtypes.GetCharLengthFromTypmod(targetType.GetAttTypMod()) + if array.BitLen() != uint(expectedLength) { + return nil, pgtypes.ErrWrongLengthBit.New(len(input), expectedLength) + } + return tree.AsStringWithFlags(array, tree.FmtPgwireText), nil + }, + }) + framework.MustAddImplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.VarBit, + ToType: pgtypes.VarBit, + Function: func(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) { + input := val.(string) + array, err := tree.ParseDBitArray(input) + if err != nil { + return nil, err + } + atttypmod := targetType.GetAttTypMod() + if atttypmod != -1 { + maxLength := pgtypes.GetCharLengthFromTypmod(atttypmod) + if int32(array.BitLen()) > maxLength { + return nil, pgtypes.ErrVarBitLengthExceeded.New(maxLength) + } + } + return tree.AsStringWithFlags(array, tree.FmtPgwireText), nil + }, + }) +} diff --git a/server/functions/binary/equal.go b/server/functions/binary/equal.go index c73469ced6..6a105eb469 100644 --- a/server/functions/binary/equal.go +++ b/server/functions/binary/equal.go @@ -33,6 +33,7 @@ import ( // initBinaryEqual registers the functions to the catalog. func initBinaryEqual() { + framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, biteq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, booleq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, bpchareq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, byteaeq) @@ -72,6 +73,7 @@ func initBinaryEqual() { framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, timestamptz_eq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, timetz_eq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, uuid_eq) + framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, varbiteq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, xideqint4) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, xideq) } @@ -523,6 +525,36 @@ func record_eq_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, return compare.CompareRecords(ctx, framework.Operator_BinaryEqual, val1, val2) } +// varbiteq represents the PostgreSQL function of the same name, taking the same parameters. +var varbiteq = framework.Function2{ + Name: "varbiteq", + Return: pgtypes.Bool, + Parameters: [2]*pgtypes.DoltgresType{pgtypes.VarBit, pgtypes.VarBit}, + Strict: true, + Callable: varbit_eq_callable, +} + +// biteq represents the PostgreSQL function of the same name, taking the same parameters. +var biteq = framework.Function2{ + Name: "biteq", + Return: pgtypes.Bool, + Parameters: [2]*pgtypes.DoltgresType{pgtypes.Bit, pgtypes.Bit}, + Strict: true, + Callable: bit_eq_callable, +} + +// varbit_eq_callable is the callable logic for the varbiteq function. +func varbit_eq_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + res, err := pgtypes.VarBit.Compare(ctx, val1, val2) + return res == 0, err +} + +// bit_eq_callable is the callable logic for the varbiteq function. +func bit_eq_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + res, err := pgtypes.Bit.Compare(ctx, val1, val2) + return res == 0, err +} + // record_eq represents the PostgreSQL function of the same name, taking the same parameters. var record_eq = framework.Function2{ Name: "record_eq", diff --git a/server/functions/bit.go b/server/functions/bit.go new file mode 100644 index 0000000000..f3073ee938 --- /dev/null +++ b/server/functions/bit.go @@ -0,0 +1,130 @@ +// Copyright 2024 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 functions + +import ( + "fmt" + + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/postgres/parser/sem/tree" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" + "github.com/dolthub/doltgresql/utils" +) + +// initBit registers the functions to the catalog. +func initBit() { + framework.RegisterFunction(bitin) + framework.RegisterFunction(bitout) + framework.RegisterFunction(bitrecv) + framework.RegisterFunction(bitsend) + framework.RegisterFunction(bittypmodin) + framework.RegisterFunction(bittypmodout) +} + +// bitin represents the PostgreSQL function of bit type IO input. +var bitin = framework.Function3{ + Name: "bit_in", + Return: pgtypes.Bit, + Parameters: [3]*pgtypes.DoltgresType{pgtypes.Cstring, pgtypes.Oid, pgtypes.Int32}, + Strict: true, + Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, val1, _, val3 any) (any, error) { + input := val1.(string) + typmod := val3.(int32) + + // validation and normalization + array, err := tree.ParseDBitArray(input) + if err != nil { + return nil, err + } + + expectedLength := pgtypes.GetCharLengthFromTypmod(typmod) + if array.BitLen() != uint(expectedLength) { + return nil, pgtypes.ErrWrongLengthBit.New(len(input), expectedLength) + } + + return tree.AsStringWithFlags(array, tree.FmtPgwireText), nil + }, +} + +// bitout represents the PostgreSQL function of bit type IO output. +var bitout = framework.Function1{ + Name: "bit_out", + Return: pgtypes.Cstring, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Bit}, + Strict: true, + Callable: func(ctx *sql.Context, t [2]*pgtypes.DoltgresType, val any) (any, error) { + bitStr := val.(string) + return bitStr, nil + }, +} + +// bitrecv represents the PostgreSQL function of bit type IO receive. +var bitrecv = framework.Function3{ + Name: "bit_recv", + Return: pgtypes.Bit, + Parameters: [3]*pgtypes.DoltgresType{pgtypes.Internal, pgtypes.Oid, pgtypes.Int32}, + Strict: true, + Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, val1, val2, val3 any) (any, error) { + data := val1.([]byte) + if len(data) == 0 { + return nil, nil + } + reader := utils.NewReader(data) + return reader.String(), nil + }, +} + +// bitsend represents the PostgreSQL function of bit type IO send. +var bitsend = framework.Function1{ + Name: "bit_send", + Return: pgtypes.Bytea, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Bit}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + str := val.(string) + wr := utils.NewWriter(uint64(4 + len(str))) + wr.String(str) + return wr.Data(), nil + }, +} + +// bittypmodin represents the PostgreSQL function of bit type IO typmod input. +var bittypmodin = framework.Function1{ + Name: "bittypmodin", + Return: pgtypes.Int32, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.CstringArray}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + return getTypModFromStringArr("bit", val.([]any)) + }, +} + +// bittypmodout represents the PostgreSQL function of bit type IO typmod output. +var bittypmodout = framework.Function1{ + Name: "bittypmodout", + Return: pgtypes.Cstring, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Int32}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + typmod := val.(int32) + if typmod < 5 { + return "", nil + } + bitLength := pgtypes.GetCharLengthFromTypmod(typmod) + return fmt.Sprintf("(%v)", bitLength), nil + }, +} diff --git a/server/functions/framework/cast.go b/server/functions/framework/cast.go index 8292c59a4f..2cf13cd1b5 100644 --- a/server/functions/framework/cast.go +++ b/server/functions/framework/cast.go @@ -130,11 +130,9 @@ func GetExplicitCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresTyp } else if tcf = getCast(implicitTypeCastMutex, implicitTypeCastsMap, fromType, toType, GetExplicitCast); tcf != nil { return tcf } - // We check for the identity after checking the maps, as the identity may be overridden (such as for types that have - // parameters). If one of the types are a string type, then we do not use the identity, and use the I/O conversions - // below. - if fromType.ID == toType.ID && toType.TypCategory != pgtypes.TypeCategory_StringTypes && fromType.TypCategory != pgtypes.TypeCategory_StringTypes { - return IdentityCast + // We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user. + if cast := getSizingOrIdentityCast(fromType, toType, true); cast != nil { + return cast } // All types have a built-in explicit cast from string types: https://www.postgresql.org/docs/15/sql-createcast.html if fromType.TypCategory == pgtypes.TypeCategory_StringTypes { @@ -172,10 +170,9 @@ func GetAssignmentCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresT } else if tcf = getCast(implicitTypeCastMutex, implicitTypeCastsMap, fromType, toType, GetAssignmentCast); tcf != nil { return tcf } - // We check for the identity after checking the maps, as the identity may be overridden (such as for types that have - // parameters). If the "to" type is a string type, then we do not use the identity, and use the I/O conversion below. - if fromType.ID == toType.ID && fromType.TypCategory != pgtypes.TypeCategory_StringTypes { - return IdentityCast + // We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user. + if cast := getSizingOrIdentityCast(fromType, toType, false); cast != nil { + return cast } // All types have a built-in assignment cast to string types: https://www.postgresql.org/docs/15/sql-createcast.html if toType.TypCategory == pgtypes.TypeCategory_StringTypes { @@ -199,10 +196,9 @@ func GetImplicitCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresTyp if tcf := getCast(implicitTypeCastMutex, implicitTypeCastsMap, fromType, toType, GetImplicitCast); tcf != nil { return tcf } - // We check for the identity after checking the maps, as the identity may be overridden (such as for types that have - // parameters). - if fromType.ID == toType.ID { - return IdentityCast + // We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user. + if cast := getSizingOrIdentityCast(fromType, toType, false); cast != nil { + return cast } return nil } @@ -282,6 +278,40 @@ func getCast(mutex *sync.RWMutex, return nil } +// getSizingOrIdentityCast returns an identity cast if the two types are exactly the same, and a sizing cast if they +// only differ in their atttypmod values. Returns nil if no functions are matched. This mirrors the behavior as described in: +// https://www.postgresql.org/docs/15/typeconv-query.html +func getSizingOrIdentityCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType, isExplicitCast bool) pgtypes.TypeCastFunction { + // If we receive different types, then we can return immediately + if fromType.ID != toType.ID { + return nil + } + // If we have different atttypmod values, then we need to do a sizing cast only if one exists + if fromType.GetAttTypMod() != toType.GetAttTypMod() { + // TODO: We don't have any sizing cast functions implemented, so for now we'll approximate using output to input. + // We can use the query below to find all implemented sizing cast functions. It's also detailed in the link above. + // Lastly, not all sizing functions accept a boolean, but for those that do, we need to see whether true is + // used for explicit casts, or whether true is used for implicit casts. + // SELECT + // format_type(c.castsource, NULL) AS source, + // format_type(c.casttarget, NULL) AS target, + // p.oid::regprocedure AS func + // FROM pg_cast c JOIN pg_proc p ON p.oid = c.castfunc WHERE c.castsource = c.casttarget ORDER BY 1,2; + return func(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) { + if val == nil { + return nil, nil + } + str, err := fromType.IoOutput(ctx, val) + if err != nil { + return nil, err + } + return targetType.IoInput(ctx, str) + } + } + // If there is no sizing cast, then we simply use the identity cast + return IdentityCast +} + // IdentityCast returns the input value. func IdentityCast(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) { return val, nil diff --git a/server/functions/init.go b/server/functions/init.go index d1a53dff85..e98cc3edc7 100644 --- a/server/functions/init.go +++ b/server/functions/init.go @@ -22,6 +22,7 @@ func initTypeFunctions() { initAnyEnum() initAnyNonArray() initArray() + initBit() initBool() initBpChar() initBytea() @@ -56,6 +57,7 @@ func initTypeFunctions() { initTimeTZ() initUnknown() initUuid() + initVarBit() initVarChar() initVoid() initXid() diff --git a/server/functions/varbit.go b/server/functions/varbit.go new file mode 100644 index 0000000000..6d94cfea97 --- /dev/null +++ b/server/functions/varbit.go @@ -0,0 +1,139 @@ +// Copyright 2024 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 functions + +import ( + "fmt" + + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/postgres/parser/sem/tree" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" + "github.com/dolthub/doltgresql/utils" +) + +// initVarBit registers the functions to the catalog. +func initVarBit() { + framework.RegisterFunction(varbitin) + framework.RegisterFunction(varbitout) + framework.RegisterFunction(varbitrecv) + framework.RegisterFunction(varbitsend) + framework.RegisterFunction(varbittypmodin) + framework.RegisterFunction(varbittypmodout) +} + +// varbitin represents the PostgreSQL function of varbit type IO input. +var varbitin = framework.Function3{ + Name: "varbit_in", + Return: pgtypes.VarBit, + Parameters: [3]*pgtypes.DoltgresType{pgtypes.Cstring, pgtypes.Oid, pgtypes.Int32}, + Strict: true, + Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, val1, _, val3 any) (any, error) { + input := val1.(string) + typmod := val3.(int32) + + // validation and normalization + bitStr, err := tree.ParseDBitArray(input) + if err != nil { + return nil, err + } + + // Check length against typmod (varbit allows up to typmod length) + if typmod != -1 { + maxLength := pgtypes.GetCharLengthFromTypmod(typmod) + if int32(bitStr.BitLen()) > maxLength { + return nil, pgtypes.ErrVarBitLengthExceeded.New(maxLength) + } + } + + return tree.AsStringWithFlags(bitStr, tree.FmtPgwireText), nil + }, +} + +// varbitout represents the PostgreSQL function of varbit type IO output. +var varbitout = framework.Function1{ + Name: "varbit_out", + Return: pgtypes.Cstring, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.VarBit}, + Strict: true, + Callable: func(ctx *sql.Context, t [2]*pgtypes.DoltgresType, val any) (any, error) { + bitStr, ok, err := sql.Unwrap[string](ctx, val) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("varbit_out function returned false") + } + return bitStr, nil + }, +} + +// varbitrecv represents the PostgreSQL function of varbit type IO receive. +var varbitrecv = framework.Function3{ + Name: "varbit_recv", + Return: pgtypes.VarBit, + Parameters: [3]*pgtypes.DoltgresType{pgtypes.Internal, pgtypes.Oid, pgtypes.Int32}, + Strict: true, + Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, val1, val2, val3 any) (any, error) { + data := val1.([]byte) + if len(data) == 0 { + return nil, nil + } + reader := utils.NewReader(data) + return reader.String(), nil + }, +} + +// varbitsend represents the PostgreSQL function of varbit type IO send. +var varbitsend = framework.Function1{ + Name: "varbit_send", + Return: pgtypes.Bytea, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.VarBit}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + bitStr := val.(string) + writer := utils.NewWriter(uint64(len(bitStr) + 4)) + writer.String(bitStr) + return writer.Data(), nil + }, +} + +// varbittypmodin represents the PostgreSQL function of varbit type IO typmod input. +var varbittypmodin = framework.Function1{ + Name: "varbittypmodin", + Return: pgtypes.Int32, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.CstringArray}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + return getTypModFromStringArr("bit varying", val.([]any)) + }, +} + +// varbittypmodout represents the PostgreSQL function of varbit type IO typmod output. +var varbittypmodout = framework.Function1{ + Name: "varbittypmodout", + Return: pgtypes.Cstring, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Int32}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + typmod := val.(int32) + if typmod < 5 { + return "", nil + } + maxLength := pgtypes.GetCharLengthFromTypmod(typmod) + return fmt.Sprintf("(%v)", maxLength), nil + }, +} diff --git a/server/functions/varchar.go b/server/functions/varchar.go index 2f67abc1e9..8064a0efef 100644 --- a/server/functions/varchar.go +++ b/server/functions/varchar.go @@ -64,7 +64,13 @@ var varcharout = framework.Function1{ Parameters: [1]*pgtypes.DoltgresType{pgtypes.VarChar}, Strict: true, Callable: func(ctx *sql.Context, t [2]*pgtypes.DoltgresType, val any) (any, error) { - v := val.(string) + v, ok, err := sql.Unwrap[string](ctx, val) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf(`"varcharout" requires a string argument, got %T`, val) + } typ := t[0] tm := typ.GetAttTypMod() if tm != -1 { diff --git a/server/tables/pgcatalog/pg_proc.go b/server/tables/pgcatalog/pg_proc.go index 8f12781c94..8965e1b4ee 100644 --- a/server/tables/pgcatalog/pg_proc.go +++ b/server/tables/pgcatalog/pg_proc.go @@ -47,7 +47,7 @@ func (p PgProcHandler) RowIter(ctx *sql.Context, partition sql.Partition) (sql.R return emptyRowIter() } -// Schema implements the interface tables.Handler. +// PkSchema implements the interface tables.Handler. func (p PgProcHandler) PkSchema() sql.PrimaryKeySchema { return sql.PrimaryKeySchema{ Schema: pgProcSchema, diff --git a/server/types/bit.go b/server/types/bit.go new file mode 100644 index 0000000000..9dbd8bab4e --- /dev/null +++ b/server/types/bit.go @@ -0,0 +1,71 @@ +// Copyright 2024 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 types + +import ( + "gopkg.in/src-d/go-errors.v1" + + "github.com/dolthub/doltgresql/core/id" +) + +// ErrWrongLengthBit is returned when a value with the incorrect length is inserted into a Bit column. +var ErrWrongLengthBit = errors.NewKind(`bit string length %d does not match type bit(%d)`) + +// Bit is a fixed-length bit string. +var Bit = &DoltgresType{ + ID: toInternal("bit"), + TypLength: int16(-1), + PassedByVal: false, + TypType: TypeType_Base, + TypCategory: TypeCategory_BitStringTypes, + IsPreferred: false, + IsDefined: true, + Delimiter: ",", + RelID: id.Null, + SubscriptFunc: toFuncID("-"), + Elem: id.NullType, + Array: toInternal("_bit"), + InputFunc: toFuncID("bit_in", toInternal("cstring"), toInternal("oid"), toInternal("int4")), + OutputFunc: toFuncID("bit_out", toInternal("bit")), + ReceiveFunc: toFuncID("bit_recv", toInternal("internal"), toInternal("oid"), toInternal("int4")), + SendFunc: toFuncID("bit_send", toInternal("bit")), + ModInFunc: toFuncID("bittypmodin", toInternal("_cstring")), + ModOutFunc: toFuncID("bittypmodout", toInternal("int4")), + AnalyzeFunc: toFuncID("-"), + Align: TypeAlignment_Int, + Storage: TypeStorage_Extended, + NotNull: false, + BaseTypeID: id.NullType, + TypMod: -1, + NDims: 0, + TypCollation: id.NewCollation("pg_catalog", "default"), + DefaulBin: "", + Default: "", + Acl: nil, + Checks: nil, + attTypMod: -1, + CompareFunc: toFuncID("bitcmp", toInternal("bit"), toInternal("bit")), +} + +// NewBitType returns a Bit type with type modifier set +// representing the number of bits in the string. +func NewBitType(width int32) (*DoltgresType, error) { + typmod, err := GetTypModFromCharLength("bit", width) + if err != nil { + return nil, err + } + newType := *Bit.WithAttTypMod(typmod) + return &newType, nil +} diff --git a/server/types/bit_array.go b/server/types/bit_array.go new file mode 100644 index 0000000000..5b749c2ec8 --- /dev/null +++ b/server/types/bit_array.go @@ -0,0 +1,18 @@ +// 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 types + +// BitArray is the array variant of VarBit. +var BitArray = CreateArrayTypeFromBaseType(Bit) diff --git a/server/types/globals.go b/server/types/globals.go index 25d3d29668..f848651e43 100644 --- a/server/types/globals.go +++ b/server/types/globals.go @@ -112,7 +112,7 @@ func GetAllBuitInTypes() []*DoltgresType { var IDToBuiltInDoltgresType = map[id.Type]*DoltgresType{ toInternal("_abstime"): Unknown, toInternal("_aclitem"): Unknown, - toInternal("_bit"): Unknown, + toInternal("_bit"): BitArray, toInternal("_bool"): BoolArray, toInternal("_box"): Unknown, toInternal("_bpchar"): BpCharArray, @@ -176,7 +176,7 @@ var IDToBuiltInDoltgresType = map[id.Type]*DoltgresType{ toInternal("_tsvector"): Unknown, toInternal("_txid_snapshot"): Unknown, toInternal("_uuid"): UuidArray, - toInternal("_varbit"): Unknown, + toInternal("_varbit"): VarBitArray, toInternal("_varchar"): VarCharArray, toInternal("_xid"): XidArray, toInternal("_xml"): Unknown, @@ -188,7 +188,7 @@ var IDToBuiltInDoltgresType = map[id.Type]*DoltgresType{ toInternal("anyenum"): AnyEnum, toInternal("anynonarray"): AnyNonArray, toInternal("anyrange"): Unknown, - toInternal("bit"): Unknown, + toInternal("bit"): Bit, toInternal("bool"): Bool, toInternal("box"): Unknown, toInternal("bpchar"): BpChar, @@ -272,7 +272,7 @@ var IDToBuiltInDoltgresType = map[id.Type]*DoltgresType{ toInternal("txid_snapshot"): Unknown, toInternal("unknown"): Unknown, toInternal("uuid"): Uuid, - toInternal("varbit"): Unknown, + toInternal("varbit"): VarBit, toInternal("varchar"): VarChar, toInternal("void"): Void, toInternal("xid"): Xid, diff --git a/server/types/varbit.go b/server/types/varbit.go new file mode 100644 index 0000000000..ba0e87c868 --- /dev/null +++ b/server/types/varbit.go @@ -0,0 +1,71 @@ +// Copyright 2024 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 types + +import ( + "gopkg.in/src-d/go-errors.v1" + + "github.com/dolthub/doltgresql/core/id" +) + +// ErrVarBitLengthExceeded is returned when a varbit value exceeds the defined length. +var ErrVarBitLengthExceeded = errors.NewKind(`bit string too long for type bit varying(%d)`) + +// VarBit is a varying-length bit string. +var VarBit = &DoltgresType{ + ID: toInternal("varbit"), + TypLength: int16(-1), + PassedByVal: false, + TypType: TypeType_Base, + TypCategory: TypeCategory_BitStringTypes, + IsPreferred: false, + IsDefined: true, + Delimiter: ",", + RelID: id.Null, + SubscriptFunc: toFuncID("-"), + Elem: id.NullType, + Array: toInternal("_varbit"), + InputFunc: toFuncID("varbit_in", toInternal("cstring"), toInternal("oid"), toInternal("int4")), + OutputFunc: toFuncID("varbit_out", toInternal("varbit")), + ReceiveFunc: toFuncID("varbit_recv", toInternal("internal"), toInternal("oid"), toInternal("int4")), + SendFunc: toFuncID("varbit_send", toInternal("varbit")), + ModInFunc: toFuncID("varbittypmodin", toInternal("_cstring")), + ModOutFunc: toFuncID("varbittypmodout", toInternal("int4")), + AnalyzeFunc: toFuncID("-"), + Align: TypeAlignment_Int, + Storage: TypeStorage_Extended, + NotNull: false, + BaseTypeID: id.NullType, + TypMod: -1, + NDims: 0, + TypCollation: id.NewCollation("pg_catalog", "default"), + DefaulBin: "", + Default: "", + Acl: nil, + Checks: nil, + attTypMod: -1, + CompareFunc: toFuncID("varbitcmp", toInternal("varbit"), toInternal("varbit")), +} + +// NewVarBitType returns a VarBit type with type modifier set +// representing the max number of bits in the string. +func NewVarBitType(width int32) (*DoltgresType, error) { + typmod, err := GetTypModFromCharLength("bit", width) + if err != nil { + return nil, err + } + newType := *VarBit.WithAttTypMod(typmod) + return &newType, nil +} diff --git a/server/types/varbit_array.go b/server/types/varbit_array.go new file mode 100644 index 0000000000..9564202be8 --- /dev/null +++ b/server/types/varbit_array.go @@ -0,0 +1,18 @@ +// 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 types + +// VarBitArray is the array variant of VarBit. +var VarBitArray = CreateArrayTypeFromBaseType(VarBit) diff --git a/testing/go/adaptive_encoding_test.go b/testing/go/adaptive_encoding_test.go index aa440e43de..54afbba81d 100644 --- a/testing/go/adaptive_encoding_test.go +++ b/testing/go/adaptive_encoding_test.go @@ -19,6 +19,7 @@ import ( "testing" "github.com/dolthub/go-mysql-server/enginetest/scriptgen/setup" + "github.com/jackc/pgx/v5/pgtype" "github.com/dolthub/go-mysql-server/sql" ) @@ -33,6 +34,30 @@ func makeTestBytes(size int, firstbyte byte) []byte { return bytes } +func makeTestVarbit(sizeInBits int) pgtype.Bits { + numBytes := sizeInBits / 8 + leftoverBits := sizeInBits % 8 + bytes := make([]byte, numBytes) + for i := 0; i < numBytes; i++ { + bytes[i] = 0xaa + } + + // this is a little annoying, but if we have leftover bits, we need to pad out the remaining bits in the last byte with 0s + endingByte := byte(0) + for i := 0; i < leftoverBits/2; i++ { + endingByte |= 0b10 << (6 - i*2) + } + if leftoverBits > 0 { + bytes = append(bytes, endingByte) + } + + return pgtype.Bits{ + Bytes: bytes, + Len: int32(sizeInBits), + Valid: true, + } +} + // A 4000 byte file starting with 0x01 and then consisting of all zeros. // This is larger than default target tuple size for outlining adaptive types. // We expect a tuple to always store this value out-of-band @@ -48,38 +73,192 @@ var halfSizeString = string(makeTestBytes(2000, 2)) // We expect a tuple to never store this value out-of-band. var tinyString = string(makeTestBytes(10, 3)) -func TestAdaptiveEncoding(t *testing.T) { - columnType := "text" +// A 4000 byte file starting with ascii byte 1 and then consisting of all zeros. +// This is larger than default target tuple size for outlining adaptive types. +// We expect a tuple to always store this value out-of-band +var fullSizeVarbit = makeTestVarbit(4000) + +// A 2000 byte file starting with ascii byte 1 and then consisting of all zeros. +// This is over half of the default target tuple size for outlining adaptive types. +// We expect a tuple to be able to store this value inline once, but not twice. +var halfSizeVarbit = makeTestVarbit(2000) + +// A 10 byte file starting with ascii byte 1 and then consisting of 10 zero bytes. +// This is file is smaller than an address hash. +// We expect a tuple to never store this value out-of-band. +var tinyVarbit = makeTestVarbit(10) + +func TestAdaptiveEncodingText(t *testing.T) { fullSizeOutOfLineRepr := fullSizeString + columnTypes := []string{"varchar", "text"} + for _, columnType := range columnTypes { + t.Run(columnType, func(t *testing.T) { + RunScripts(t, []ScriptTest{ + { + Name: "Adaptive Encoding With One Column", + SetUpScript: setup.SetupScript{ + fmt.Sprintf(`create table blobt (i char(1) primary key, b %s);`, columnType), + fmt.Sprintf(`create table blobt2 (i char(2) primary key, b1 %s, b2 %s);`, columnType, columnType), + `insert into blobt values + ('F', LOAD_FILE('testdata/fullSize')), + ('H', LOAD_FILE('testdata/halfSize')), + ('T', LOAD_FILE('testdata/tinyFile'))`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "select b from blobt where i = 'F'", + Expected: []sql.Row{{fullSizeString}}, + }, + { + // Files that can fit within a tuple are stored inline. + Query: "select b from blobt where i = 'H'", + Expected: []sql.Row{{halfSizeString}}, + }, + { + // An inlined adaptive column can be used in a filter. + Query: "select i from blobt where b = LOAD_FILE('testdata/fullSize')", + Expected: []sql.Row{{"F"}}, + }, + { + // An out-of-line adaptive column can be used in a filter. + Query: "select i from blobt where b = LOAD_FILE('testdata/halfSize')", + Expected: []sql.Row{{"H"}}, + }, + }, + }, + { + Name: "Adaptive Encoding With Two Columns", + SetUpScript: setup.SetupScript{ + fmt.Sprintf(`create table blobt2 (i char(2) primary key, b1 %s, b2 %s);`, columnType, columnType), + `insert into blobt2 values + ('FF', LOAD_FILE('testdata/fullSize'), LOAD_FILE('testdata/fullSize')), + ('HF', LOAD_FILE('testdata/halfSize'), LOAD_FILE('testdata/fullSize')), + ('TF', LOAD_FILE('testdata/tinyFile'), LOAD_FILE('testdata/fullSize')), + ('FH', LOAD_FILE('testdata/fullSize'), LOAD_FILE('testdata/halfSize')), + ('HH', LOAD_FILE('testdata/halfSize'), LOAD_FILE('testdata/halfSize')), + ('TH', LOAD_FILE('testdata/tinyFile'), LOAD_FILE('testdata/halfSize')), + ('FT', LOAD_FILE('testdata/fullSize'), LOAD_FILE('testdata/tinyFile')), + ('HT', LOAD_FILE('testdata/halfSize'), LOAD_FILE('testdata/tinyFile')), + ('TT', LOAD_FILE('testdata/tinyFile'), LOAD_FILE('testdata/tinyFile'))`, + }, + Assertions: []ScriptTestAssertion{ + { + // When a tuple with multiple adaptive columns is too large, columns are moved out-of-band from left to right. + // However, strings smaller than the address size (20 bytes) are never stored out-of-band. + Query: "select i, b1, b2 from blobt2", + Expected: []sql.Row{ + {"FF", fullSizeString, fullSizeString}, + {"HF", halfSizeString, fullSizeString}, + {"TF", tinyString, fullSizeString}, + {"FH", fullSizeString, halfSizeString}, + {"HH", halfSizeString, halfSizeString}, + {"TH", tinyString, halfSizeString}, + {"FT", fullSizeString, tinyString}, + {"HT", halfSizeString, tinyString}, + {"TT", tinyString, tinyString}, + }, + }, + { + // An adaptive column can be used in a filter when it doesn't have the same encoding in all rows. + Query: "select i from blobt2 where b1 = LOAD_FILE('testdata/halfSize')", + Expected: []sql.Row{{"HF"}, {"HH"}, {"HT"}}, + }, + { + // An adaptive column can be used in a filter when it doesn't have the same encoding in all rows. + Query: "select i from blobt2 where b2 = LOAD_FILE('testdata/halfSize')", + Expected: []sql.Row{{"FH"}, {"HH"}, {"TH"}}, + }, + { + // Test creating an index on an adaptive encoding column, matching against out-of-band values + Query: "CREATE INDEX bidx ON blobt2 (b1)", + }, + { + Query: "select i, b1 FROM blobt2 WHERE b1 LIKE '\x01%'", + Expected: []sql.Row{ + {"FF", fullSizeOutOfLineRepr}, + {"FH", fullSizeOutOfLineRepr}, + {"FT", fullSizeOutOfLineRepr}, + }, + }, + { + // Test creating an index on an adaptive encoding column, matching against inline values + Query: "CREATE INDEX bidx2 ON blobt2 (b2)", + }, + { + Query: "select i, b2 FROM blobt2 WHERE b2 LIKE '\x02%'", + Expected: []sql.Row{ + {"FH", halfSizeString}, + {"HH", halfSizeString}, + {"TH", halfSizeString}, + }, + }, + { + // Tuples containing adaptive columns should be independent of how the tuple was created. + // And adaptive values are always outlined starting from the left. + // This means that in a table with two adaptive columns where both columns were previously stored out-of line, + // Decreasing the size of the second column may allow both columns to be stored inline. + Query: "UPDATE blobt2 SET b2 = LOAD_FILE('testdata/tinyFile') WHERE i = 'HH'", + }, + { + Query: "select i, b1, b2 from blobt2 where i = 'HH'", + Expected: []sql.Row{{"HH", halfSizeString, tinyString}}, + }, + { + // Similar to the above, dropping a column can change whether the other column is inlined. + Query: "ALTER TABLE blobt2 DROP COLUMN b2", + }, + { + Query: "select i, b1 from blobt2", + Expected: []sql.Row{ + {"FF", fullSizeString}, + {"HF", halfSizeString}, + {"TF", tinyString}, + {"FH", fullSizeString}, + {"HH", halfSizeString}, + {"TH", tinyString}, + {"FT", fullSizeString}, + {"HT", halfSizeString}, + {"TT", tinyString}, + }, + }, + }, + }, + }) + }) + } +} + +func TestAdaptiveEncodingVarbit(t *testing.T) { + columnType := "varbit" RunScripts(t, []ScriptTest{ { Name: "Adaptive Encoding With One Column", SetUpScript: setup.SetupScript{ fmt.Sprintf(`create table blobt (i char(1) primary key, b %s);`, columnType), fmt.Sprintf(`create table blobt2 (i char(2) primary key, b1 %s, b2 %s);`, columnType, columnType), - `insert into blobt values - ('F', LOAD_FILE('testdata/fullSize')), - ('H', LOAD_FILE('testdata/halfSize')), - ('T', LOAD_FILE('testdata/tinyFile'))`, + fmt.Sprintf(`insert into blobt values + ('F', LOAD_FILE('testdata/fullSizeVarbit')::%s), + ('H', LOAD_FILE('testdata/halfSizeVarbit')::%s), + ('T', LOAD_FILE('testdata/tinyFileVarbit')::%s)`, columnType, columnType, columnType), }, Assertions: []ScriptTestAssertion{ { Query: "select b from blobt where i = 'F'", - Expected: []sql.Row{{fullSizeString}}, + Expected: []sql.Row{{fullSizeVarbit}}, }, { // Files that can fit within a tuple are stored inline. Query: "select b from blobt where i = 'H'", - Expected: []sql.Row{{halfSizeString}}, + Expected: []sql.Row{{halfSizeVarbit}}, }, { // An inlined adaptive column can be used in a filter. - Query: "select i from blobt where b = LOAD_FILE('testdata/fullSize')", + Query: "select i from blobt where b = LOAD_FILE('testdata/fullSizeVarbit')::varbit", Expected: []sql.Row{{"F"}}, }, { // An out-of-line adaptive column can be used in a filter. - Query: "select i from blobt where b = LOAD_FILE('testdata/halfSize')", + Query: "select i from blobt where b = LOAD_FILE('testdata/halfSizeVarbit')::varbit", Expected: []sql.Row{{"H"}}, }, }, @@ -88,97 +267,46 @@ func TestAdaptiveEncoding(t *testing.T) { Name: "Adaptive Encoding With Two Columns", SetUpScript: setup.SetupScript{ fmt.Sprintf(`create table blobt2 (i char(2) primary key, b1 %s, b2 %s);`, columnType, columnType), - `insert into blobt2 values - ('FF', LOAD_FILE('testdata/fullSize'), LOAD_FILE('testdata/fullSize')), - ('HF', LOAD_FILE('testdata/halfSize'), LOAD_FILE('testdata/fullSize')), - ('TF', LOAD_FILE('testdata/tinyFile'), LOAD_FILE('testdata/fullSize')), - ('FH', LOAD_FILE('testdata/fullSize'), LOAD_FILE('testdata/halfSize')), - ('HH', LOAD_FILE('testdata/halfSize'), LOAD_FILE('testdata/halfSize')), - ('TH', LOAD_FILE('testdata/tinyFile'), LOAD_FILE('testdata/halfSize')), - ('FT', LOAD_FILE('testdata/fullSize'), LOAD_FILE('testdata/tinyFile')), - ('HT', LOAD_FILE('testdata/halfSize'), LOAD_FILE('testdata/tinyFile')), - ('TT', LOAD_FILE('testdata/tinyFile'), LOAD_FILE('testdata/tinyFile'))`, + fmt.Sprintf(`insert into blobt2 values + ('FF', LOAD_FILE('testdata/fullSizeVarbit')::%s, LOAD_FILE('testdata/fullSizeVarbit')::%s), + ('HF', LOAD_FILE('testdata/halfSizeVarbit')::%s, LOAD_FILE('testdata/fullSizeVarbit')::%s), + ('TF', LOAD_FILE('testdata/tinyFileVarbit')::%s, LOAD_FILE('testdata/fullSizeVarbit')::%s), + ('FH', LOAD_FILE('testdata/fullSizeVarbit')::%s, LOAD_FILE('testdata/halfSizeVarbit')::%s), + ('HH', LOAD_FILE('testdata/halfSizeVarbit')::%s, LOAD_FILE('testdata/halfSizeVarbit')::%s), + ('TH', LOAD_FILE('testdata/tinyFileVarbit')::%s, LOAD_FILE('testdata/halfSizeVarbit')::%s), + ('FT', LOAD_FILE('testdata/fullSizeVarbit')::%s, LOAD_FILE('testdata/tinyFileVarbit')::%s), + ('HT', LOAD_FILE('testdata/halfSizeVarbit')::%s, LOAD_FILE('testdata/tinyFileVarbit')::%s), + ('TT', LOAD_FILE('testdata/tinyFileVarbit')::%s, LOAD_FILE('testdata/tinyFileVarbit')::%s)`, columnType, columnType, + columnType, columnType, columnType, columnType, columnType, columnType, columnType, columnType, + columnType, columnType, columnType, columnType, columnType, columnType, columnType, columnType), }, Assertions: []ScriptTestAssertion{ { // When a tuple with multiple adaptive columns is too large, columns are moved out-of-band from left to right. // However, strings smaller than the address size (20 bytes) are never stored out-of-band. - Query: "select i, b1, b2 from blobt2", + Query: "select i, b1, b2 from blobt2 order by 1", Expected: []sql.Row{ - {"FF", fullSizeString, fullSizeString}, - {"HF", halfSizeString, fullSizeString}, - {"TF", tinyString, fullSizeString}, - {"FH", fullSizeString, halfSizeString}, - {"HH", halfSizeString, halfSizeString}, - {"TH", tinyString, halfSizeString}, - {"FT", fullSizeString, tinyString}, - {"HT", halfSizeString, tinyString}, - {"TT", tinyString, tinyString}, + {"FF", fullSizeVarbit, fullSizeVarbit}, + {"FH", fullSizeVarbit, halfSizeVarbit}, + {"FT", fullSizeVarbit, tinyVarbit}, + {"HF", halfSizeVarbit, fullSizeVarbit}, + {"HH", halfSizeVarbit, halfSizeVarbit}, + {"HT", halfSizeVarbit, tinyVarbit}, + {"TF", tinyVarbit, fullSizeVarbit}, + {"TH", tinyVarbit, halfSizeVarbit}, + {"TT", tinyVarbit, tinyVarbit}, }, }, { // An adaptive column can be used in a filter when it doesn't have the same encoding in all rows. - Query: "select i from blobt2 where b1 = LOAD_FILE('testdata/halfSize')", + Query: "select i from blobt2 where b1 = LOAD_FILE('testdata/halfSizeVarbit')::varbit", Expected: []sql.Row{{"HF"}, {"HH"}, {"HT"}}, }, { // An adaptive column can be used in a filter when it doesn't have the same encoding in all rows. - Query: "select i from blobt2 where b2 = LOAD_FILE('testdata/halfSize')", + Query: "select i from blobt2 where b2 = LOAD_FILE('testdata/halfSizeVarbit')::varbit", Expected: []sql.Row{{"FH"}, {"HH"}, {"TH"}}, }, - { - // Test creating an index on an adaptive encoding column, matching against out-of-band values - Query: "CREATE INDEX bidx ON blobt2 (b1)", - }, - { - Query: "select i, b1 FROM blobt2 WHERE b1 LIKE '\x01%'", - Expected: []sql.Row{ - {"FF", fullSizeOutOfLineRepr}, - {"FH", fullSizeOutOfLineRepr}, - {"FT", fullSizeOutOfLineRepr}, - }, - }, - { - // Test creating an index on an adaptive encoding column, matching against inline values - Query: "CREATE INDEX bidx2 ON blobt2 (b2)", - }, - { - Query: "select i, b2 FROM blobt2 WHERE b2 LIKE '\x02%'", - Expected: []sql.Row{ - {"FH", halfSizeString}, - {"HH", halfSizeString}, - {"TH", halfSizeString}, - }, - }, - { - // Tuples containing adaptive columns should be independent of how the tuple was created. - // And adaptive values are always outlined starting from the left. - // This means that in a table with two adaptive columns where both columns were previously stored out-of line, - // Decreasing the size of the second column may allow both columns to be stored inline. - Query: "UPDATE blobt2 SET b2 = LOAD_FILE('testdata/tinyFile') WHERE i = 'HH'", - }, - { - Query: "select i, b1, b2 from blobt2 where i = 'HH'", - Expected: []sql.Row{{"HH", halfSizeString, tinyString}}, - }, - { - // Similar to the above, dropping a column can change whether the other column is inlined. - Query: "ALTER TABLE blobt2 DROP COLUMN b2", - }, - { - Query: "select i, b1 from blobt2", - Expected: []sql.Row{ - {"FF", fullSizeString}, - {"HF", halfSizeString}, - {"TF", tinyString}, - {"FH", fullSizeString}, - {"HH", halfSizeString}, - {"TH", tinyString}, - {"FT", fullSizeString}, - {"HT", halfSizeString}, - {"TT", tinyString}, - }, - }, }, }, }) diff --git a/testing/go/testdata/fullSizeVarbit b/testing/go/testdata/fullSizeVarbit new file mode 100644 index 0000000000..31c4bf3149 --- /dev/null +++ b/testing/go/testdata/fullSizeVarbit @@ -0,0 +1 @@ +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 \ No newline at end of file diff --git a/testing/go/testdata/halfSizeVarbit b/testing/go/testdata/halfSizeVarbit new file mode 100644 index 0000000000..26082f03a5 --- /dev/null +++ b/testing/go/testdata/halfSizeVarbit @@ -0,0 +1 @@ +10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 \ No newline at end of file diff --git a/testing/go/testdata/tinyFileVarbit b/testing/go/testdata/tinyFileVarbit new file mode 100644 index 0000000000..3ba845cfe2 --- /dev/null +++ b/testing/go/testdata/tinyFileVarbit @@ -0,0 +1 @@ +1010101010 \ No newline at end of file diff --git a/testing/go/types_test.go b/testing/go/types_test.go index 6014048317..fa98c6a6df 100644 --- a/testing/go/types_test.go +++ b/testing/go/types_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/dolthub/go-mysql-server/sql" + "github.com/jackc/pgx/v5/pgtype" ) func TestTypes(t *testing.T) { @@ -79,24 +80,42 @@ var typesTests = []ScriptTest{ }, { Name: "Bit type", - Skip: true, // no pgx support: unknown type with oid: 1560 SetUpScript: []string{ - "CREATE TABLE t_bit (id INTEGER primary key, v1 BIT(8));", - "INSERT INTO t_bit VALUES (1, B'11011010'), (2, B'00101011');", + "CREATE TABLE t_bit (id INTEGER primary key, v1 BIT(8), v2 BIT(3));", + "INSERT INTO t_bit VALUES (1, B'11011010', '101'), (2, B'00101011', '000');", }, Assertions: []ScriptTestAssertion{ { Query: "SELECT * FROM t_bit ORDER BY id;", Expected: []sql.Row{ - {1, []byte{0xDA}}, - {2, []byte{0x2B}}, + {1, pgtype.Bits{Bytes: []uint8{0xda}, Len: 8, Valid: true}, pgtype.Bits{Bytes: []uint8{0xa0}, Len: 3, Valid: true}}, + {2, pgtype.Bits{Bytes: []uint8{0x2b}, Len: 8, Valid: true}, pgtype.Bits{Bytes: []uint8{0x0}, Len: 3, Valid: true}}, }, }, + { + Query: "INSERT INTO t_bit VALUES (3, B'101', '111');", + ExpectedErr: "bit string length 3 does not match type bit(8)", + }, + { + Query: "INSERT INTO t_bit VALUES (3, B'1001000110', '111');", + ExpectedErr: "bit string length 10 does not match type bit(8)", + }, + { + Query: "INSERT INTO t_bit VALUES (3, B'10010001', '11100100');", + ExpectedErr: "bit string length 8 does not match type bit(3)", + }, + { + Query: "INSERT INTO t_bit VALUES (3, B'10012345', '111');", + ExpectedErr: "not a valid binary digit", + }, + { + Query: "INSERT INTO t_bit VALUES (3, '10012345', '111');", + ExpectedErr: "not a valid binary digit", + }, }, }, { Name: "Bit key", - Skip: true, // no pgx support: unknown type with oid: 1560 SetUpScript: []string{ "CREATE TABLE t_bit (id BIT(8) primary key, v1 BIT(8));", "INSERT INTO t_bit VALUES (B'11011010', B'11011010'), (B'00101011', B'00101011');", @@ -105,7 +124,7 @@ var typesTests = []ScriptTest{ { Query: "SELECT * FROM t_bit WHERE id = B'11011010' ORDER BY id;", Expected: []sql.Row{ - {[]byte{0xDA}, []byte{0xDA}}, + {pgtype.Bits{Bytes: []uint8{0xda}, Len: 8, Valid: true}, pgtype.Bits{Bytes: []uint8{0xda}, Len: 8, Valid: true}}, }, }, }, @@ -282,7 +301,6 @@ var typesTests = []ScriptTest{ }, { Name: "Bit varying type", - Skip: true, SetUpScript: []string{ "CREATE TABLE t_bit_varying (id INTEGER primary key, v1 BIT VARYING(16));", "INSERT INTO t_bit_varying VALUES (1, B'1101101010101010'), (2, B'0010101101010101');", @@ -291,8 +309,37 @@ var typesTests = []ScriptTest{ { Query: "SELECT * FROM t_bit_varying ORDER BY id;", Expected: []sql.Row{ - {1, []byte{0xDA, 0xAA}}, - {2, []byte{0x2B, 0xA5}}, + {1, pgtype.Bits{Bytes: []uint8{0xda, 0xaa}, Len: 16, Valid: true}}, + {2, pgtype.Bits{Bytes: []uint8{0x2b, 0x55}, Len: 16, Valid: true}}, + }, + }, + { + Query: "INSERT INTO t_bit_varying VALUES (3, B'101010101010101010');", + ExpectedErr: "bit string too long for type bit varying(16)", + }, + }, + }, + { + Name: "Bit varying type, unbounded", + SetUpScript: []string{ + "CREATE TABLE t_bit_varying (id INTEGER primary key, v1 BIT VARYING);", + "INSERT INTO t_bit_varying VALUES (1, B'1101101010101010'), (2, B'0010101101010101');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT * FROM t_bit_varying ORDER BY id;", + Expected: []sql.Row{ + {1, pgtype.Bits{Bytes: []uint8{0xda, 0xaa}, Len: 16, Valid: true}}, + {2, pgtype.Bits{Bytes: []uint8{0x2b, 0x55}, Len: 16, Valid: true}}, + }, + }, + { + Query: "INSERT INTO t_bit_varying VALUES (3, B'101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010');", + }, + { + Query: "SELECT * FROM t_bit_varying WHERE id = 3 order by 1;", + Expected: []sql.Row{ + {3, pgtype.Bits{Bytes: []uint8{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xa0}, Len: 108, Valid: true}}, }, }, },