From f35c4aa272499f4dd1532bf6e1ddca0ac4606db1 Mon Sep 17 00:00:00 2001 From: Daylon Wilkins Date: Fri, 11 Sep 2026 02:38:59 -0700 Subject: [PATCH] Fixes many issues --- server/ast/alter_table.go | 1 + server/ast/column_table_def.go | 30 +- server/ast/resolvable_type_reference.go | 2 + server/ast/select.go | 17 +- server/ast/table_def.go | 2 +- server/auth/auth_handler.go | 31 +- server/cast/char.go | 10 +- server/cast/init.go | 1 + server/cast/int16.go | 10 + server/cast/int32.go | 10 + server/cast/int64.go | 13 + server/cast/oid.go | 7 + server/cast/regnamespace.go | 59 ++++ server/compare/utils.go | 36 ++ server/expression/binary_operator.go | 4 +- server/expression/is_distinct_from.go | 5 + server/expression/is_not_distinct_from.go | 5 + server/expression/unary_operator.go | 4 +- server/functions/bpchar.go | 2 +- server/functions/convert_from.go | 87 +++++ server/functions/decode.go | 98 ++++++ .../framework/interpreted_function.go | 57 ++-- server/functions/init.go | 4 + server/functions/regnamespace.go | 125 +++++++ server/functions/to_regnamespace.go | 56 ++++ server/plpgsql/interpreter_stack.go | 19 ++ .../information_schema/columns_table.go | 31 +- server/tables/information_schema/init.go | 5 +- .../information_schema/triggers_table.go | 163 +++++++++ server/types/globals.go | 4 +- server/types/regnamespace.go | 74 +++++ server/types/regnamespace_array.go | 18 + server/types/type.go | 11 +- .../command_docs/output/alter_table_test.go | 2 +- testing/go/alter_table_test.go | 2 +- testing/go/dolt_tables_test.go | 8 +- .../go/enginetest/doltgres_harness_test.go | 7 - testing/go/enginetest/query_converter_test.go | 4 +- testing/go/extensions/pgvector_knn_test.go | 6 +- .../go/functional_index_multi_expr_test.go | 4 +- testing/go/index_test.go | 22 +- testing/go/issues_test.go | 310 ++++++++++++++++++ testing/go/pgcatalog_test.go | 65 ++-- testing/go/stats_usage_test.go | 8 +- testing/go/subqueries_test.go | 4 +- 45 files changed, 1304 insertions(+), 139 deletions(-) create mode 100644 server/cast/regnamespace.go create mode 100644 server/functions/convert_from.go create mode 100644 server/functions/decode.go create mode 100644 server/functions/regnamespace.go create mode 100644 server/functions/to_regnamespace.go create mode 100644 server/tables/information_schema/triggers_table.go create mode 100644 server/types/regnamespace.go create mode 100644 server/types/regnamespace_array.go diff --git a/server/ast/alter_table.go b/server/ast/alter_table.go index 3c47018cc7..7ba6bbc92a 100644 --- a/server/ast/alter_table.go +++ b/server/ast/alter_table.go @@ -134,6 +134,7 @@ func nodeAlterTableCmds( } statement.IndexSpec = &vitess.IndexSpec{ Action: "create", + ToName: vitess.NewColIdent(string(cmd.ColumnDef.UniqueConstraintName)), Type: "unique", Fields: indexFields, } diff --git a/server/ast/column_table_def.go b/server/ast/column_table_def.go index 283b333110..5c8e0c7c20 100644 --- a/server/ast/column_table_def.go +++ b/server/ast/column_table_def.go @@ -32,11 +32,6 @@ func nodeColumnTableDef(ctx *Context, node *tree.ColumnTableDef) (*vitess.Column if node == nil { return nil, nil } - if len(node.Nullable.ConstraintName) > 0 || - len(node.DefaultExpr.ConstraintName) > 0 || - len(node.UniqueConstraintName) > 0 { - return nil, errors.Errorf("non-foreign key column constraint names are not yet supported") - } convertType, resolvedType, err := nodeResolvableTypeReference(ctx, node.Type, false) if err != nil { return nil, err @@ -110,15 +105,9 @@ func nodeColumnTableDef(ctx *Context, node *tree.ColumnTableDef) (*vitess.Column } } - if generated != nil { - // GMS requires the AST to wrap function expressions in parens - if _, ok := generated.(*vitess.FuncExpr); ok { - generated = &vitess.ParenExpr{Expr: generated} - } - - // clean up the expressions generated here. our default expression handling generates aliases that aren't - // appropriate in this context. - generated = clearAliases(generated) + // GMS requires the AST to wrap function expressions in parens + if _, ok := generated.(*vitess.FuncExpr); ok { + generated = &vitess.ParenExpr{Expr: generated} } if node.IsSerial || computedByDefaultAsIdentity || computedAsIdentity { @@ -181,16 +170,3 @@ func nodeColumnTableDef(ctx *Context, node *tree.ColumnTableDef) (*vitess.Column } return colDef, nil } - -// clearAliases removes As and InputExpression from any AliasedExpr in the expression tree given. This is required -// in some contexts where we expect the expression to serialize to a string without any alias names. -func clearAliases(e vitess.Expr) vitess.Expr { - _ = vitess.Walk(func(node vitess.SQLNode) (kontinue bool, err error) { - if expr, ok := node.(*vitess.AliasedExpr); ok { - expr.As = vitess.ColIdent{} - expr.InputExpression = "" - } - return true, nil - }, e) - return e -} diff --git a/server/ast/resolvable_type_reference.go b/server/ast/resolvable_type_reference.go index 2edff55ea4..d13a5fbdb4 100644 --- a/server/ast/resolvable_type_reference.go +++ b/server/ast/resolvable_type_reference.go @@ -164,6 +164,8 @@ func nodeResolvableTypeReference(ctx *Context, typ tree.ResolvableTypeReference, doltgresType = pgtypes.Oidvector case oid.T_regclass: doltgresType = pgtypes.Regclass + case oid.T_regnamespace: + doltgresType = pgtypes.Regnamespace case oid.T_regproc: doltgresType = pgtypes.Regproc case oid.T_regtype: diff --git a/server/ast/select.go b/server/ast/select.go index 05c8e898f2..718e0831df 100644 --- a/server/ast/select.go +++ b/server/ast/select.go @@ -237,9 +237,13 @@ func nodeExprToSelectExpr(ctx *Context, node tree.Expr) (vitess.SelectExpr, erro if node == nil { return nil, nil } - return nodeSelectExpr(ctx, tree.SelectExpr{ + selectExpr, err := nodeSelectExpr(ctx, tree.SelectExpr{ Expr: node, }) + if err != nil { + return nil, err + } + return clearArgumentAlias(selectExpr), nil } // nodeExprsToSelectExprs handles tree.Exprs nodes and returns the results as vitess.SelectExprs. @@ -256,6 +260,17 @@ func nodeExprsToSelectExprs(ctx *Context, node tree.Exprs) (vitess.SelectExprs, if err != nil { return nil, err } + selectExprs[i] = clearArgumentAlias(selectExprs[i]) } return selectExprs, nil } + +// clearArgumentAlias removes the alias that `nodeSelectExpr` gives a function argument, which would otherwise be +// written back to text as "x as x". +func clearArgumentAlias(node vitess.SelectExpr) vitess.SelectExpr { + if aliasedExpr, ok := node.(*vitess.AliasedExpr); ok { + aliasedExpr.As = vitess.ColIdent{} + aliasedExpr.InputExpression = "" + } + return node +} diff --git a/server/ast/table_def.go b/server/ast/table_def.go index 7fc28370fd..bf56f42e2e 100644 --- a/server/ast/table_def.go +++ b/server/ast/table_def.go @@ -61,7 +61,7 @@ func assignTableDef(ctx *Context, node tree.TableDef, target *vitess.DDL) error return err } target.TableSpec.Indexes = append(target.TableSpec.Indexes, &vitess.IndexDefinition{ - Info: &vitess.IndexInfo{Unique: true}, + Info: &vitess.IndexInfo{Name: vitess.NewColIdent(string(node.UniqueConstraintName)), Unique: true}, Fields: indexFields, }) } diff --git a/server/auth/auth_handler.go b/server/auth/auth_handler.go index 7b091b48ee..19f17d6835 100644 --- a/server/auth/auth_handler.go +++ b/server/auth/auth_handler.go @@ -23,7 +23,7 @@ import ( vitess "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/dolthub/doltgresql/core" - "github.com/dolthub/doltgresql/server/functions/framework" + "github.com/dolthub/doltgresql/core/id" ) // AuthorizationQueryState contains any cached state for a query. @@ -373,11 +373,12 @@ func checkPrivilegeOnRoutine(ctx *sql.Context, state AuthorizationQueryState, sc } for _, privilege := range privileges { if !HasRoutinePrivilege(roleRoutineKey, privilege) && !HasRoutinePrivilege(publicRoutineKey, privilege) { - // check if it's system function - _, ok := framework.Catalog[strings.ToLower(routineName)] - if ok && schemaName == "" { - // TODO: for now we don't check privilege for pg_catalog tables as it's granted for PUBLIC by default - // need to fix it when we support 'REVOKE privileges FROM PUBLIC' + userDefined, err := isUserDefinedRoutine(ctx, schName, routineName) + if err != nil { + return err + } + if !userDefined { + //TODO: built-in routines are granted to PUBLIC by default, so deny them once REVOKE ... FROM PUBLIC is supported return nil } return errors.Errorf("permission denied for routine %s", routineName) @@ -385,3 +386,21 @@ func checkPrivilegeOnRoutine(ctx *sql.Context, state AuthorizationQueryState, sc } return nil } + +// isUserDefinedRoutine returns whether a function or procedure with the given name exists in the given schema. +func isUserDefinedRoutine(ctx *sql.Context, schemaName string, routineName string) (bool, error) { + funcCollection, err := core.GetFunctionsCollectionFromContext(ctx, "") + if err != nil { + return false, err + } + funcOverloads, err := funcCollection.GetFunctionOverloads(ctx, id.NewFunction(schemaName, routineName)) + if err != nil || len(funcOverloads) > 0 { + return len(funcOverloads) > 0, err + } + procCollection, err := core.GetProceduresCollectionFromContext(ctx, "") + if err != nil { + return false, err + } + procOverloads, err := procCollection.GetProcedureOverloads(ctx, id.NewProcedure(schemaName, routineName)) + return len(procOverloads) > 0, err +} diff --git a/server/cast/char.go b/server/cast/char.go index 3c09dc01a8..370972a710 100644 --- a/server/cast/char.go +++ b/server/cast/char.go @@ -92,14 +92,18 @@ func charImplicit(builtInCasts map[id.Cast]casts.Cast) { if err != nil { return nil, err } - return handleStringCast(str, targetType) + return handleStringCast(strings.TrimRight(str, " "), targetType) }, }) framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ FromType: pgtypes.BpChar, ToType: pgtypes.Text, Function: func(ctx *sql.Context, val any, _, targetType *pgtypes.DoltgresType) (any, error) { - return val, nil + str, err := framework.UnwrapString(ctx, val) + if err != nil { + return nil, err + } + return strings.TrimRight(str, " "), nil }, }) framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ @@ -110,7 +114,7 @@ func charImplicit(builtInCasts map[id.Cast]casts.Cast) { if err != nil { return nil, err } - return handleStringCast(str, targetType) + return handleStringCast(strings.TrimRight(str, " "), targetType) }, }) } diff --git a/server/cast/init.go b/server/cast/init.go index 632a4f9f00..0295ef2f2a 100644 --- a/server/cast/init.go +++ b/server/cast/init.go @@ -38,6 +38,7 @@ func Init(builtInCasts map[id.Cast]casts.Cast) { initNumeric(builtInCasts) initOid(builtInCasts) initRegclass(builtInCasts) + initRegnamespace(builtInCasts) initRegproc(builtInCasts) initRegtype(builtInCasts) initText(builtInCasts) diff --git a/server/cast/int16.go b/server/cast/int16.go index efa1f3fd27..6dbc7718cf 100644 --- a/server/cast/int16.go +++ b/server/cast/int16.go @@ -87,6 +87,16 @@ func int16Implicit(builtInCasts map[id.Cast]casts.Cast) { return id.NewOID(uint32(val.(int16))).AsId(), nil }, }) + framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ + FromType: pgtypes.Int16, + ToType: pgtypes.Regnamespace, + Function: func(ctx *sql.Context, val any, _, targetType *pgtypes.DoltgresType) (any, error) { + if internalID := id.Cache().ToInternal(uint32(val.(int16))); internalID.IsValid() { + return internalID, nil + } + return id.NewOID(uint32(val.(int16))).AsId(), nil + }, + }) framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ FromType: pgtypes.Int16, ToType: pgtypes.Regproc, diff --git a/server/cast/int32.go b/server/cast/int32.go index 5e95521918..485bc9345a 100644 --- a/server/cast/int32.go +++ b/server/cast/int32.go @@ -128,6 +128,16 @@ func int32Implicit(builtInCasts map[id.Cast]casts.Cast) { return id.NewOID(uint32(val.(int32))).AsId(), nil }, }) + framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ + FromType: pgtypes.Int32, + ToType: pgtypes.Regnamespace, + Function: func(ctx *sql.Context, val any, _, targetType *pgtypes.DoltgresType) (any, error) { + if internalID := id.Cache().ToInternal(uint32(val.(int32))); internalID.IsValid() { + return internalID, nil + } + return id.NewOID(uint32(val.(int32))).AsId(), nil + }, + }) framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ FromType: pgtypes.Int32, ToType: pgtypes.Regproc, diff --git a/server/cast/int64.go b/server/cast/int64.go index 5d38f6c4b9..ae7b58c1b4 100644 --- a/server/cast/int64.go +++ b/server/cast/int64.go @@ -106,6 +106,19 @@ func int64Implicit(builtInCasts map[id.Cast]casts.Cast) { return id.NewOID(uint32(val.(int64))).AsId(), nil }, }) + framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ + FromType: pgtypes.Int64, + ToType: pgtypes.Regnamespace, + Function: func(ctx *sql.Context, val any, _, targetType *pgtypes.DoltgresType) (any, error) { + if val.(int64) > int64(math.MaxUint32) || val.(int64) < 0 { + return nil, errOutOfRange.New(targetType.String()) + } + if internalID := id.Cache().ToInternal(uint32(val.(int64))); internalID.IsValid() { + return internalID, nil + } + return id.NewOID(uint32(val.(int64))).AsId(), nil + }, + }) framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ FromType: pgtypes.Int64, ToType: pgtypes.Regproc, diff --git a/server/cast/oid.go b/server/cast/oid.go index 5b08b7cda2..8a90bf8daf 100644 --- a/server/cast/oid.go +++ b/server/cast/oid.go @@ -56,6 +56,13 @@ func oidImplicit(builtInCasts map[id.Cast]casts.Cast) { return val, nil }, }) + framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ + FromType: pgtypes.Oid, + ToType: pgtypes.Regnamespace, + Function: func(ctx *sql.Context, val any, _, targetType *pgtypes.DoltgresType) (any, error) { + return val, nil + }, + }) framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ FromType: pgtypes.Oid, ToType: pgtypes.Regproc, diff --git a/server/cast/regnamespace.go b/server/cast/regnamespace.go new file mode 100644 index 0000000000..3f2d318690 --- /dev/null +++ b/server/cast/regnamespace.go @@ -0,0 +1,59 @@ +// 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/core/casts" + "github.com/dolthub/doltgresql/core/id" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initRegnamespace handles all casts that are built-in. This comprises only the source types. +func initRegnamespace(builtInCasts map[id.Cast]casts.Cast) { + regnamespaceAssignment(builtInCasts) + regnamespaceImplicit(builtInCasts) +} + +// regnamespaceAssignment registers all assignment casts. This comprises only the source types. +func regnamespaceAssignment(builtInCasts map[id.Cast]casts.Cast) { + framework.MustAddAssignmentTypeCast(builtInCasts, framework.TypeCast{ + FromType: pgtypes.Regnamespace, + ToType: pgtypes.Int32, + Function: func(ctx *sql.Context, val any, _, targetType *pgtypes.DoltgresType) (any, error) { + return int32(id.Cache().ToOID(val.(id.Id))), nil + }, + }) + framework.MustAddAssignmentTypeCast(builtInCasts, framework.TypeCast{ + FromType: pgtypes.Regnamespace, + ToType: pgtypes.Int64, + Function: func(ctx *sql.Context, val any, _, targetType *pgtypes.DoltgresType) (any, error) { + return int64(id.Cache().ToOID(val.(id.Id))), nil + }, + }) +} + +// regnamespaceImplicit registers all implicit casts. This comprises only the source types. +func regnamespaceImplicit(builtInCasts map[id.Cast]casts.Cast) { + framework.MustAddImplicitTypeCast(builtInCasts, framework.TypeCast{ + FromType: pgtypes.Regnamespace, + ToType: pgtypes.Oid, + Function: func(ctx *sql.Context, val any, _, targetType *pgtypes.DoltgresType) (any, error) { + return val, nil + }, + }) +} diff --git a/server/compare/utils.go b/server/compare/utils.go index d4ec02799b..be4a109200 100644 --- a/server/compare/utils.go +++ b/server/compare/utils.go @@ -142,3 +142,39 @@ func callComparisonFunction(ctx *sql.Context, op framework.Operator, leftLiteral ctx, "_internal_record_comparison_function", leftLiteral, rightLiteral) return compiledFunction.Eval(ctx, nil) } + +// RecordsAreDistinct returns whether two records differ in any field, with NULL fields only equal to each other. +func RecordsAreDistinct(ctx *sql.Context, v1 interface{}, v2 interface{}) (bool, error) { + leftRecord, rightRecord, err := checkRecordArgs(v1, v2) + if err != nil { + return false, err + } + var leftLiteral, rightLiteral expression.Literal + for i := 0; i < len(leftRecord); i++ { + if leftRecord[i].Value == nil || rightRecord[i].Value == nil { + if leftRecord[i].Value != nil || rightRecord[i].Value != nil { + return true, nil + } + continue + } + if _, ok := leftRecord[i].Value.([]pgtypes.RecordValue); ok { + distinct, err := RecordsAreDistinct(ctx, leftRecord[i].Value, rightRecord[i].Value) + if err != nil || distinct { + return distinct, err + } + continue + } + leftLiteral.Val = leftRecord[i].Value + leftLiteral.Typ = leftRecord[i].Type + rightLiteral.Val = rightRecord[i].Value + rightLiteral.Typ = rightRecord[i].Type + res, err := callComparisonFunction(ctx, framework.Operator_BinaryNotEqual, &leftLiteral, &rightLiteral) + if err != nil { + return false, err + } + if res == true { + return true, nil + } + } + return false, nil +} diff --git a/server/expression/binary_operator.go b/server/expression/binary_operator.go index 4a7c1e560a..e75136336f 100644 --- a/server/expression/binary_operator.go +++ b/server/expression/binary_operator.go @@ -96,10 +96,10 @@ func (b *BinaryOperator) String() string { // We know that we'll always have two parameters here switch f := b.compiledFunc.(type) { case *framework.CompiledFunction: - return fmt.Sprintf("%s %s %s", + return fmt.Sprintf("(%s %s %s)", f.Arguments[0].String(), b.operator.String(), f.Arguments[1].String()) case *framework.QuickFunction2: - return fmt.Sprintf("%s %s %s", + return fmt.Sprintf("(%s %s %s)", f.Arguments[0].String(), b.operator.String(), f.Arguments[1].String()) default: return fmt.Sprintf("unexpected binary operator function type: %T", b.compiledFunc) diff --git a/server/expression/is_distinct_from.go b/server/expression/is_distinct_from.go index b0f61d9268..ec5f8f77e5 100644 --- a/server/expression/is_distinct_from.go +++ b/server/expression/is_distinct_from.go @@ -22,6 +22,7 @@ import ( "github.com/dolthub/go-mysql-server/sql/expression" vitess "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/dolthub/doltgresql/server/compare" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) @@ -68,6 +69,10 @@ func (n *IsDistinctFrom) Eval(ctx *sql.Context, row sql.Row) (any, error) { } else if left == nil || right == nil { return true, nil } + if _, ok := left.([]pgtypes.RecordValue); ok { + distinct, err := compare.RecordsAreDistinct(ctx, left, right) + return distinct, err + } n.staticLeftLiteral.Val = left n.staticRightLiteral.Val = right diff --git a/server/expression/is_not_distinct_from.go b/server/expression/is_not_distinct_from.go index 82eef30966..16a781af87 100644 --- a/server/expression/is_not_distinct_from.go +++ b/server/expression/is_not_distinct_from.go @@ -22,6 +22,7 @@ import ( "github.com/dolthub/go-mysql-server/sql/expression" vitess "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/dolthub/doltgresql/server/compare" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) @@ -68,6 +69,10 @@ func (n *IsNotDistinctFrom) Eval(ctx *sql.Context, row sql.Row) (any, error) { } else if left == nil || right == nil { return false, nil } + if _, ok := left.([]pgtypes.RecordValue); ok { + distinct, err := compare.RecordsAreDistinct(ctx, left, right) + return !distinct, err + } n.staticLeftLiteral.Val = left n.staticRightLiteral.Val = right diff --git a/server/expression/unary_operator.go b/server/expression/unary_operator.go index d76cb66c7e..1b92a016b9 100644 --- a/server/expression/unary_operator.go +++ b/server/expression/unary_operator.go @@ -67,9 +67,9 @@ func (b *UnaryOperator) String() string { // We know that we'll always have one parameter here switch f := b.compiledFunc.(type) { case *framework.CompiledFunction: - return fmt.Sprintf("%s%s", b.operator.String(), f.Arguments[0].String()) + return fmt.Sprintf("(%s%s)", b.operator.String(), f.Arguments[0].String()) case *framework.QuickFunction1: - return fmt.Sprintf("%s%s", b.operator.String(), f.Argument.String()) + return fmt.Sprintf("(%s%s)", b.operator.String(), f.Argument.String()) default: return fmt.Sprintf("unexpected unary operator function type: %T", b.compiledFunc) } diff --git a/server/functions/bpchar.go b/server/functions/bpchar.go index 75218f1216..1e6d069f45 100644 --- a/server/functions/bpchar.go +++ b/server/functions/bpchar.go @@ -183,7 +183,7 @@ var bpcharcmp = framework.Function2{ if err != nil { return nil, err } - return int32(bytes.Compare([]byte(val1Str), []byte(val2Str))), nil + return int32(bytes.Compare([]byte(strings.TrimRight(val1Str, " ")), []byte(strings.TrimRight(val2Str, " ")))), nil }, } diff --git a/server/functions/convert_from.go b/server/functions/convert_from.go new file mode 100644 index 0000000000..8508117cef --- /dev/null +++ b/server/functions/convert_from.go @@ -0,0 +1,87 @@ +// 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 functions + +import ( + "fmt" + "unicode/utf8" + + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/postgres/parser/pgcode" + "github.com/dolthub/doltgresql/postgres/parser/pgerror" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initConvertFrom registers the functions to the catalog. +func initConvertFrom() { + framework.RegisterFunction(convert_from_bytea_name) +} + +// convert_from_bytea_name represents the PostgreSQL function of the same name, taking the same parameters. +var convert_from_bytea_name = framework.Function2{ + Name: "convert_from", + Return: pgtypes.Text, + Parameters: [2]*pgtypes.DoltgresType{pgtypes.Bytea, pgtypes.Name}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) { + input, err := framework.UnwrapBytes(ctx, val1) + if err != nil { + return nil, err + } + encodingName, err := framework.UnwrapString(ctx, val2) + if err != nil { + return nil, err + } + + source := lookupPostgresEncoding(encodingName) + if source == nil { + return nil, pgerror.WithCandidateCode( + fmt.Errorf(`invalid source encoding name "%s"`, encodingName), pgcode.InvalidParameterValue) + } + if source.passThrough { + if err = validUTF8(input); err != nil { + return nil, err + } + return string(input), nil + } + if source.encoder == nil { + return nil, pgerror.WithCandidateCode(fmt.Errorf( + `source encoding "%s" is recognized but not yet supported; request support at %s`, + source.name, encodingSupportIssuesURL), pgcode.FeatureNotSupported) + } + + converted, err := source.encoder.NewDecoder().Bytes(input) + if err != nil { + return nil, pgerror.WithCandidateCode( + fmt.Errorf(`invalid byte sequence for encoding "%s"`, source.name), pgcode.CharacterNotInRepertoire) + } + return string(converted), nil + }, +} + +// validUTF8 returns an error naming the first byte that is not part of a valid UTF-8 sequence. +func validUTF8(input []byte) error { + for i := 0; i < len(input); { + r, size := utf8.DecodeRune(input[i:]) + if r == utf8.RuneError && size <= 1 { + return pgerror.WithCandidateCode( + fmt.Errorf(`invalid byte sequence for encoding "UTF8": 0x%02x`, input[i]), pgcode.CharacterNotInRepertoire) + } + i += size + } + return nil +} diff --git a/server/functions/decode.go b/server/functions/decode.go new file mode 100644 index 0000000000..1baaf66dd6 --- /dev/null +++ b/server/functions/decode.go @@ -0,0 +1,98 @@ +// 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 functions + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/cockroachdb/errors" + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/postgres/parser/pgcode" + "github.com/dolthub/doltgresql/postgres/parser/pgerror" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initDecode registers the functions to the catalog. +func initDecode() { + framework.RegisterFunction(decode) +} + +// errInvalidByteaSyntax is returned when the "escape" format input cannot be decoded. +var errInvalidByteaSyntax = pgerror.WithCandidateCode( + errors.New("invalid input syntax for type bytea"), pgcode.InvalidTextRepresentation) + +// decode represents the PostgreSQL function of the same name, taking the same parameters. +var decode = framework.Function2{ + Name: "decode", + Return: pgtypes.Bytea, + Parameters: [2]*pgtypes.DoltgresType{pgtypes.Text, pgtypes.Text}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) { + data, err := framework.UnwrapString(ctx, val1) + if err != nil { + return nil, err + } + format, err := framework.UnwrapString(ctx, val2) + if err != nil { + return nil, err + } + switch strings.ToLower(format) { + case "hex": + decoded, err := hex.DecodeString(strings.Join(strings.Fields(data), "")) + var invalidByte hex.InvalidByteError + if errors.As(err, &invalidByte) { + return nil, pgerror.WithCandidateCode( + fmt.Errorf(`invalid hexadecimal digit: "%c"`, rune(invalidByte)), pgcode.InvalidParameterValue) + } else if err != nil { + return nil, pgerror.WithCandidateCode( + errors.New("invalid hexadecimal data: odd number of digits"), pgcode.InvalidParameterValue) + } + return decoded, nil + case "base64": + decoded, err := base64.StdEncoding.DecodeString(strings.Join(strings.Fields(data), "")) + if err != nil { + return nil, pgerror.WithCandidateCode( + errors.New("invalid symbol found while decoding base64 sequence"), pgcode.InvalidParameterValue) + } + return decoded, nil + case "escape": + decoded := make([]byte, 0, len(data)) + for i := 0; i < len(data); i++ { + if data[i] != '\\' { + decoded = append(decoded, data[i]) + } else if i+1 < len(data) && data[i+1] == '\\' { + decoded = append(decoded, '\\') + i++ + } else if i+3 >= len(data) { + return nil, errInvalidByteaSyntax + } else if b, err := strconv.ParseUint(data[i+1:i+4], 8, 8); err != nil { + return nil, errInvalidByteaSyntax + } else { + decoded = append(decoded, byte(b)) + i += 3 + } + } + return decoded, nil + default: + return nil, fmt.Errorf(`unrecognized encoding: "%s"`, format) + } + }, +} diff --git a/server/functions/framework/interpreted_function.go b/server/functions/framework/interpreted_function.go index 5f668c6305..50934e7996 100644 --- a/server/functions/framework/interpreted_function.go +++ b/server/functions/framework/interpreted_function.go @@ -316,32 +316,51 @@ func (InterpretedFunction) ApplyBindings(ctx *sql.Context, stack plpgsql.Interpr if variable.Type == nil { return newStmt, false, plpgsql.ErrVariableNotFound.New(bindingName) } - var formattedVar string - if *variable.Value != nil { - formattedVar, err = variable.Type.FormatValueWithContext(ctx, *variable.Value) + formattedVar, err := formatBinding(ctx, variable.Type, *variable.Value, enforceType) + if err != nil { + return newStmt, true, err + } + newStmt = strings.ReplaceAll(newStmt, "$"+strconv.Itoa(i+1), formattedVar) + } + return newStmt, true, nil +} + +// formatBinding returns the SQL text for a binding's value, casting it when `enforceType` is set. A record becomes a +// ROW constructor over its formatted fields. +func formatBinding(ctx *sql.Context, typ *pgtypes.DoltgresType, value any, enforceType bool) (string, error) { + if typ.ID == pgtypes.Record.ID { + fields := value.([]pgtypes.RecordValue) + formattedFields := make([]string, len(fields)) + for i, field := range fields { + var err error + formattedFields[i], err = formatBinding(ctx, field.Type.(*pgtypes.DoltgresType), field.Value, enforceType) if err != nil { - return newStmt, true, err + return "", err } - if enforceType { - switch variable.Type.TypCategory { - case pgtypes.TypeCategory_ArrayTypes, pgtypes.TypeCategory_CompositeTypes, pgtypes.TypeCategory_DateTimeTypes, pgtypes.TypeCategory_StringTypes, pgtypes.TypeCategory_UserDefinedTypes: - formattedVar = pq.QuoteLiteral(formattedVar) - } - } - } else { - formattedVar = "NULL" + } + return fmt.Sprintf("ROW(%s)", strings.Join(formattedFields, ", ")), nil + } + formattedVar := "NULL" + if value != nil { + var err error + formattedVar, err = typ.FormatValueWithContext(ctx, value) + if err != nil { + return "", err } if enforceType { - if variable.Type.TypCategory == pgtypes.TypeCategory_CompositeTypes { - newStmt = strings.ReplaceAll(newStmt, "$"+strconv.Itoa(i+1), fmt.Sprintf(`(%s::%s)`, formattedVar, variable.Type.String())) - } else { - newStmt = strings.ReplaceAll(newStmt, "$"+strconv.Itoa(i+1), fmt.Sprintf(`((%s)::%s)`, formattedVar, variable.Type.String())) + switch typ.TypCategory { + case pgtypes.TypeCategory_ArrayTypes, pgtypes.TypeCategory_CompositeTypes, pgtypes.TypeCategory_DateTimeTypes, pgtypes.TypeCategory_StringTypes, pgtypes.TypeCategory_UserDefinedTypes: + formattedVar = pq.QuoteLiteral(formattedVar) } - } else { - newStmt = strings.ReplaceAll(newStmt, "$"+strconv.Itoa(i+1), formattedVar) } } - return newStmt, true, nil + if !enforceType { + return formattedVar, nil + } + if typ.TypCategory == pgtypes.TypeCategory_CompositeTypes { + return fmt.Sprintf(`(%s::%s)`, formattedVar, typ.String()), nil + } + return fmt.Sprintf(`((%s)::%s)`, formattedVar, typ.String()), nil } // enforceInterfaceInheritance implements the interface FunctionInterface. diff --git a/server/functions/init.go b/server/functions/init.go index e3b3fbfaf0..b4a9cf1af8 100644 --- a/server/functions/init.go +++ b/server/functions/init.go @@ -34,6 +34,7 @@ func initTypeFunctions() { initDatePart() initDateTrunc() initDomain() + initDecode() initEncode() initEnum() initFloat4() @@ -52,6 +53,7 @@ func initTypeFunctions() { initOidvector() initRecord() initRegclass() + initRegnamespace() initRegproc() initRegtype() initShell() @@ -100,6 +102,7 @@ func Init() { initChr() initColDescription() initConcat() + initConvertFrom() initConvertTo() initCos() initCosd() @@ -231,6 +234,7 @@ func Init() { initToJson() initToJsonB() initToRegclass() + initToRegnamespace() initToRegproc() initToRegtype() initToDate() diff --git a/server/functions/regnamespace.go b/server/functions/regnamespace.go new file mode 100644 index 0000000000..2d856dc038 --- /dev/null +++ b/server/functions/regnamespace.go @@ -0,0 +1,125 @@ +// 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 functions + +import ( + "strconv" + + "github.com/cockroachdb/errors" + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/core/id" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" + "github.com/dolthub/doltgresql/utils" +) + +// initRegnamespace registers the functions to the catalog. +func initRegnamespace() { + framework.RegisterFunction(regnamespacein) + framework.RegisterFunction(regnamespaceout) + framework.RegisterFunction(regnamespacerecv) + framework.RegisterFunction(regnamespacesend) +} + +// regnamespacein represents the PostgreSQL function of regnamespace type IO input. +var regnamespacein = framework.Function1{ + Name: "regnamespacein", + Return: pgtypes.Regnamespace, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Cstring}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + input, err := framework.UnwrapString(ctx, val) + if err != nil { + return nil, err + } + if parsedOid, err := strconv.ParseUint(input, 10, 32); err == nil { + if internalID := id.Cache().ToInternal(uint32(parsedOid)); internalID.IsValid() { + return internalID, nil + } + return id.NewOID(uint32(parsedOid)).AsId(), nil + } + sections, err := ioInputSections(input) + if err != nil { + return id.Null, err + } + if len(sections) != 1 { + return id.Null, errors.Errorf("invalid name syntax") + } + + var resultOid id.Id + err = IterateCurrentDatabase(ctx, Callbacks{ + Schema: func(ctx *sql.Context, schema ItemSchema) (cont bool, err error) { + if schema.Item.SchemaName() == sections[0] { + resultOid = schema.OID.AsId() + return false, nil + } + return true, nil + }, + }) + if err != nil || resultOid.IsValid() { + return resultOid, err + } + return id.Null, errors.Errorf(`schema "%s" does not exist`, sections[0]) + }, +} + +// regnamespaceout represents the PostgreSQL function of regnamespace type IO output. +var regnamespaceout = framework.Function1{ + Name: "regnamespaceout", + Return: pgtypes.Cstring, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Regnamespace}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + input := val.(id.Id) + if input.Section() == id.Section_OID { + return input.Segment(0), nil + } + return id.Namespace(input).SchemaName(), nil + }, +} + +// regnamespacerecv represents the PostgreSQL function of regnamespace type IO receive. +var regnamespacerecv = framework.Function1{ + Name: "regnamespacerecv", + Return: pgtypes.Regnamespace, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Internal}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + data, err := framework.UnwrapBytes(ctx, val) + if err != nil { + return nil, err + } + if data == nil { + return nil, nil + } + reader := utils.NewWireReader(data) + cachedID := id.Cache().ToInternal(reader.ReadUint32()) + return cachedID, nil + }, +} + +// regnamespacesend represents the PostgreSQL function of regnamespace type IO send. +var regnamespacesend = framework.Function1{ + Name: "regnamespacesend", + Return: pgtypes.Bytea, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Regnamespace}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + writer := utils.NewWireWriter() + writer.WriteUint32(id.Cache().ToOID(val.(id.Id))) + return writer.BufferData(), nil + }, +} diff --git a/server/functions/to_regnamespace.go b/server/functions/to_regnamespace.go new file mode 100644 index 0000000000..4f6666c11d --- /dev/null +++ b/server/functions/to_regnamespace.go @@ -0,0 +1,56 @@ +// 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 functions + +import ( + "strconv" + "strings" + + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initToRegnamespace registers the functions to the catalog. +func initToRegnamespace() { + framework.RegisterFunction(to_regnamespace_text) +} + +// to_regnamespace_text represents the PostgreSQL function of the same name, taking the same parameters. +var to_regnamespace_text = framework.Function1{ + Name: "to_regnamespace", + Return: pgtypes.Regnamespace, + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Text}, + IsNonDeterministic: true, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val1 any) (any, error) { + val1Str, err := framework.UnwrapString(ctx, val1) + if err != nil { + return nil, err + } + if _, err := strconv.ParseUint(val1Str, 10, 32); err == nil { + return nil, nil + } + oid, err := pgtypes.Regnamespace.IoInput(ctx, val1Str) + if err != nil { + if strings.Contains(err.Error(), "does not exist") { + return nil, nil + } + return nil, err + } + return oid, nil + }, +} diff --git a/server/plpgsql/interpreter_stack.go b/server/plpgsql/interpreter_stack.go index 2e05304c73..a79d0c1c71 100644 --- a/server/plpgsql/interpreter_stack.go +++ b/server/plpgsql/interpreter_stack.go @@ -240,6 +240,13 @@ func (is *InterpreterStack) GetVariableWithError(name string) (InterpreterVariab Value: &iv.Value, }, nil } else if len(iv.Record) > 0 { + if fieldName == "*" { + var record any = recordValues(iv.Record, iv.Value.(sql.Row)) + return InterpreterVariableReference{ + Type: pgtypes.Record, + Value: &record, + }, nil + } fieldIdx := recordFieldIndex(iv.Record, fieldName) if fieldIdx == -1 { return InterpreterVariableReference{}, ErrRecordHasNoField.New(name, fieldName) @@ -275,6 +282,18 @@ func (is *InterpreterStack) GetVariableWithError(name string) (InterpreterVariab return InterpreterVariableReference{}, ErrVariableNotFound.New(fullName) } +// recordValues pairs each field of a record variable with its type, which is the value of a `name.*` reference. +func recordValues(sch sql.Schema, row sql.Row) []pgtypes.RecordValue { + values := make([]pgtypes.RecordValue, len(row)) + for i := range row { + values[i] = pgtypes.RecordValue{ + Value: row[i], + Type: sch[i].Type, + } + } + return values +} + // findVariable returns the variable named |name|, searching from the top of the stack down so that an inner // declaration shadows an outer one. Returns nil when no scope holds the name. A name that matches nothing // exactly is retried folded when it names a caller-supplied variable, which is what lets operations compiled diff --git a/server/tables/information_schema/columns_table.go b/server/tables/information_schema/columns_table.go index 5a697a4e7b..7a689f99d0 100644 --- a/server/tables/information_schema/columns_table.go +++ b/server/tables/information_schema/columns_table.go @@ -149,6 +149,10 @@ func getRowFromColumn(ctx *sql.Context, curOrdPos int, col *sql.Column, catName, datetimePrecision := getDatetimePrecision(col.Type) columnDefault := information_schema.GetColumnDefault(ctx, col.Default) + var generationExpression any + if col.Generated != nil { + generationExpression = trimEnclosingParens(col.Generated.String()) + } return sql.Row{ catName, // table_catalog @@ -193,11 +197,36 @@ func getRowFromColumn(ctx *sql.Context, curOrdPos int, col *sql.Column, catName, nil, // identity_minimum TODO "NO", // identity_cycle TODO isGenerated, // is_generated - nil, // generation_expression TODO + generationExpression, // generation_expression "YES", // is_updatable } } +// trimEnclosingParens removes every pair of parentheses that wraps the entire expression. +func trimEnclosingParens(expr string) string { + for len(expr) > 1 && expr[0] == '(' && expr[len(expr)-1] == ')' && parenDepthStaysPositive(expr[1:len(expr)-1]) { + expr = expr[1 : len(expr)-1] + } + return expr +} + +// parenDepthStaysPositive returns whether `expr` never closes a parenthesis that it did not open. +func parenDepthStaysPositive(expr string) bool { + depth := 0 + for _, r := range expr { + switch r { + case '(': + depth++ + case ')': + depth-- + if depth < 0 { + return false + } + } + } + return true +} + // getRowsFromTable returns array of rows for all accessible columns of the given table. func getRowsFromTable(ctx *sql.Context, db information_schema.DbWithNames, t sql.Table, allColsWithDefaultValue sql.Schema) ([]sql.Row, error) { var rows []sql.Row diff --git a/server/tables/information_schema/init.go b/server/tables/information_schema/init.go index 7f222f74cd..633ba2932b 100644 --- a/server/tables/information_schema/init.go +++ b/server/tables/information_schema/init.go @@ -29,7 +29,8 @@ func Init() { // Postgres-specific tables/views to be added to information_schema database information_schema.NewInformationSchemaTablesToAdd = map[string]sql.Table{ - ConstraintColumnUsageViewName: newConstraintColumnUsageView(), - SequencesTableName: newSequencesTable(), + ConstraintColumnUsageViewName: newConstraintColumnUsageView(), + SequencesTableName: newSequencesTable(), + information_schema.TriggersTableName: newTriggersTable(), } } diff --git a/server/tables/information_schema/triggers_table.go b/server/tables/information_schema/triggers_table.go new file mode 100644 index 0000000000..dabd1f6536 --- /dev/null +++ b/server/tables/information_schema/triggers_table.go @@ -0,0 +1,163 @@ +// 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 information_schema + +import ( + "fmt" + "sort" + "strings" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/information_schema" + "github.com/lib/pq" + + "github.com/dolthub/doltgresql/core" + "github.com/dolthub/doltgresql/core/triggers" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// newTriggersTable creates a new information_schema.TRIGGERS table. +func newTriggersTable() *information_schema.InformationSchemaTable { + return &information_schema.InformationSchemaTable{ + TableName: information_schema.TriggersTableName, + TableSchema: triggersSchema, + Reader: triggersRowIter, + } +} + +// triggersSchema is the schema for the information_schema.TRIGGERS table. +var triggersSchema = sql.Schema{ + {Name: "trigger_catalog", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "trigger_schema", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "trigger_name", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "event_manipulation", Type: character_data, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "event_object_catalog", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "event_object_schema", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "event_object_table", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_order", Type: cardinal_number, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_condition", Type: character_data, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_statement", Type: character_data, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_orientation", Type: character_data, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_timing", Type: character_data, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_reference_old_table", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_reference_new_table", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_reference_old_row", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "action_reference_new_row", Type: sql_identifier, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, + {Name: "created", Type: pgtypes.TimestampTZ, Default: nil, Nullable: true, Source: information_schema.TriggersTableName}, +} + +// triggersRowIter implements the sql.RowIter for the information_schema.TRIGGERS table. +func triggersRowIter(ctx *sql.Context, catalog sql.Catalog) (sql.RowIter, error) { + collection, err := core.GetTriggersCollectionFromContext(ctx, "") + if err != nil { + return nil, err + } + var trigs []triggers.Trigger + err = collection.IterateTriggers(ctx, func(t triggers.Trigger) (stop bool, err error) { + trigs = append(trigs, t) + return false, nil + }) + if err != nil { + return nil, err + } + sort.Slice(trigs, func(i, j int) bool { + if trigs[i].ID.SchemaName() != trigs[j].ID.SchemaName() { + return trigs[i].ID.SchemaName() < trigs[j].ID.SchemaName() + } + if trigs[i].ID.TableName() != trigs[j].ID.TableName() { + return trigs[i].ID.TableName() < trigs[j].ID.TableName() + } + return trigs[i].ID.TriggerName() < trigs[j].ID.TriggerName() + }) + + catName := ctx.GetCurrentDatabase() + actionOrders := make(map[string]int32) + var rows []sql.Row + for _, t := range trigs { + orientation := "STATEMENT" + if t.ForEachRow { + orientation = "ROW" + } + timing := triggerTiming(t.Timing) + args := make([]string, len(t.Arguments)) + for i, arg := range t.Arguments { + args[i] = pq.QuoteLiteral(arg) + } + statement := fmt.Sprintf("EXECUTE FUNCTION %s(%s)", t.Function.FunctionName(), strings.Join(args, ", ")) + var oldTable any + if len(t.OldTransitionName) > 0 { + oldTable = t.OldTransitionName + } + var newTable any + if len(t.NewTransitionName) > 0 { + newTable = t.NewTransitionName + } + for _, event := range t.Events { + manipulation := triggerEventManipulation(event.Type) + if len(manipulation) == 0 { + continue + } + orderKey := strings.Join([]string{t.ID.SchemaName(), t.ID.TableName(), manipulation, orientation, timing}, "\x00") + actionOrders[orderKey]++ + //TODO: action_condition needs the WHEN condition, which is only kept in its compiled form + rows = append(rows, sql.Row{ + catName, // trigger_catalog + t.ID.SchemaName(), // trigger_schema + t.ID.TriggerName(), // trigger_name + manipulation, // event_manipulation + catName, // event_object_catalog + t.ID.SchemaName(), // event_object_schema + t.ID.TableName(), // event_object_table + actionOrders[orderKey], // action_order + nil, // action_condition + statement, // action_statement + orientation, // action_orientation + timing, // action_timing + oldTable, // action_reference_old_table + newTable, // action_reference_new_table + nil, // action_reference_old_row + nil, // action_reference_new_row + nil, // created + }) + } + } + return sql.RowsToRowIter(rows...), nil +} + +// triggerTiming returns the action_timing value for the given trigger timing. +func triggerTiming(timing triggers.TriggerTiming) string { + switch timing { + case triggers.TriggerTiming_Before: + return "BEFORE" + case triggers.TriggerTiming_After: + return "AFTER" + default: + return "INSTEAD OF" + } +} + +// triggerEventManipulation returns the event_manipulation value for the given event type, which is empty for TRUNCATE. +func triggerEventManipulation(eventType triggers.TriggerEventType) string { + switch eventType { + case triggers.TriggerEventType_Insert: + return "INSERT" + case triggers.TriggerEventType_Update: + return "UPDATE" + case triggers.TriggerEventType_Delete: + return "DELETE" + default: + return "" + } +} diff --git a/server/types/globals.go b/server/types/globals.go index d6a2d22aa9..32c53c518f 100644 --- a/server/types/globals.go +++ b/server/types/globals.go @@ -173,7 +173,7 @@ func init() { toInternal("_regclass"): RegclassArray, toInternal("_regconfig"): Unknown, toInternal("_regdictionary"): Unknown, - toInternal("_regnamespace"): Unknown, + toInternal("_regnamespace"): RegnamespaceArray, toInternal("_regoper"): Unknown, toInternal("_regoperator"): Unknown, toInternal("_regproc"): RegprocArray, @@ -265,7 +265,7 @@ func init() { toInternal("regclass"): Regclass, toInternal("regconfig"): Unknown, toInternal("regdictionary"): Unknown, - toInternal("regnamespace"): Unknown, + toInternal("regnamespace"): Regnamespace, toInternal("regoper"): Unknown, toInternal("regoperator"): Unknown, toInternal("regproc"): Regproc, diff --git a/server/types/regnamespace.go b/server/types/regnamespace.go new file mode 100644 index 0000000000..c851f930b7 --- /dev/null +++ b/server/types/regnamespace.go @@ -0,0 +1,74 @@ +// 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 types + +import ( + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/core/id" +) + +// Regnamespace is the OID type for finding items in pg_namespace. +var Regnamespace = &DoltgresType{ + ID: toInternal("regnamespace"), + TypLength: int16(4), + PassedByVal: true, + TypType: TypeType_Base, + TypCategory: TypeCategory_NumericTypes, + IsPreferred: false, + IsDefined: true, + Delimiter: ",", + RelID: id.Null, + SubscriptFunc: toFuncID("-"), + Elem: internalNullType, + Array: internalNullType, + InputFunc: toFuncID("regnamespacein", toInternal("cstring")), + OutputFunc: toFuncID("regnamespaceout", toInternal("regnamespace")), + ReceiveFunc: toFuncID("regnamespacerecv", toInternal("internal")), + SendFunc: toFuncID("regnamespacesend", toInternal("regnamespace")), + ModInFunc: toFuncID("-"), + ModOutFunc: toFuncID("-"), + AnalyzeFunc: toFuncID("-"), + Align: TypeAlignment_Int, + Storage: TypeStorage_Plain, + NotNull: false, + BaseTypeType: internalNullType, + TypMod: -1, + NDims: 0, + TypCollation: id.NullCollation, + DefaulBin: "", + Default: "", + Acl: nil, + Checks: nil, + attTypMod: -1, + CompareFunc: toFuncID("-"), + SerializationFunc: serializeTypeRegnamespace, + DeserializationFunc: deserializeTypeRegnamespace, +} + +// serializeTypeRegnamespace handles serialization from the standard representation to our serialized representation +// that is written in Dolt. +func serializeTypeRegnamespace(ctx *sql.Context, t *DoltgresType, val any) ([]byte, error) { + return []byte(val.(id.Id)), nil +} + +// deserializeTypeRegnamespace handles deserialization from the Dolt serialized format to our standard representation +// used by expressions and nodes. +func deserializeTypeRegnamespace(ctx *sql.Context, t *DoltgresType, data []byte) (any, error) { + if len(data) == 0 { + return nil, nil + } + return id.Id(data), nil +} diff --git a/server/types/regnamespace_array.go b/server/types/regnamespace_array.go new file mode 100644 index 0000000000..24a89c452b --- /dev/null +++ b/server/types/regnamespace_array.go @@ -0,0 +1,18 @@ +// 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 types + +// RegnamespaceArray is the array variant of Regnamespace. +var RegnamespaceArray = CreateArrayTypeFromBaseType(Regnamespace) diff --git a/server/types/type.go b/server/types/type.go index 40bac87def..747dcd109a 100644 --- a/server/types/type.go +++ b/server/types/type.go @@ -21,6 +21,7 @@ import ( "fmt" "math" "reflect" + "strings" "sync" "time" @@ -384,6 +385,10 @@ func (t *DoltgresType) Compare(ctx context.Context, v1 interface{}, v2 interface } case string: bb := v2.(string) + if t.ID == BpChar.ID { + ab = strings.TrimRight(ab, " ") + bb = strings.TrimRight(bb, " ") + } if ab == bb { return 0, nil } else if ab < bb { @@ -567,7 +572,7 @@ func (t *DoltgresType) Convert(ctx context.Context, v interface{}) (interface{}, if ok { return v, sql.InRange, nil } - case "oid", "regclass", "regproc", "regtype": + case "oid", "regclass", "regnamespace", "regproc", "regtype": if _, ok := v.(id.Id); ok { return v, sql.InRange, nil } @@ -1170,7 +1175,7 @@ func (t *DoltgresType) Type() query.Type { return sqltypes.Decimal case "oid": return sqltypes.VarChar - case "regclass", "regproc", "regtype": + case "regclass", "regnamespace", "regproc", "regtype": return sqltypes.Text default: // TODO @@ -1264,7 +1269,7 @@ func (t *DoltgresType) Zero() interface{} { return int64(0) case "numeric": return apd.New(0, 0) - case "oid", "regclass", "regproc", "regtype": + case "oid", "regclass", "regnamespace", "regproc", "regtype": return id.Null default: // TODO diff --git a/testing/generation/command_docs/output/alter_table_test.go b/testing/generation/command_docs/output/alter_table_test.go index 473e843155..551fb6a332 100755 --- a/testing/generation/command_docs/output/alter_table_test.go +++ b/testing/generation/command_docs/output/alter_table_test.go @@ -8107,7 +8107,7 @@ func TestAlterTable(t *testing.T) { Parses("ALTER TABLE name * ADD IF NOT EXISTS column_name data_type COLLATE en_US CONSTRAINT constraint_name REFERENCES reftable MATCH SIMPLE ON DELETE SET NULL ( column_name , column_name ) ON UPDATE SET DEFAULT ( column_name , column_name ) DEFERRABLE CONSTRAINT constraint_name REFERENCES reftable MATCH FULL ON DELETE NO ACTION ON UPDATE SET NULL ( column_name , column_name ) NOT DEFERRABLE INITIALLY DEFERRED , ADD column_name data_type REFERENCES reftable ( refcolumn ) MATCH PARTIAL ON DELETE SET NULL ( column_name ) NOT DEFERRABLE CONSTRAINT constraint_name REFERENCES reftable MATCH SIMPLE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE"), Parses("ALTER TABLE IF EXISTS name * ADD column_name data_type COLLATE en_US CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH FULL ON DELETE NO ACTION ON UPDATE RESTRICT NOT DEFERRABLE INITIALLY IMMEDIATE CONSTRAINT constraint_name REFERENCES reftable MATCH SIMPLE ON DELETE SET NULL ON UPDATE NO ACTION DEFERRABLE INITIALLY IMMEDIATE , ADD COLUMN column_name data_type CONSTRAINT constraint_name REFERENCES reftable MATCH PARTIAL ON DELETE SET DEFAULT ON UPDATE SET DEFAULT DEFERRABLE INITIALLY DEFERRED REFERENCES reftable ( refcolumn ) MATCH SIMPLE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE"), Parses("ALTER TABLE IF EXISTS name * ADD IF NOT EXISTS column_name data_type CONSTRAINT constraint_name REFERENCES reftable MATCH PARTIAL ON UPDATE SET DEFAULT ( column_name ) DEFERRABLE INITIALLY DEFERRED CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) ON DELETE SET DEFAULT ( column_name , column_name ) ON UPDATE SET NULL INITIALLY IMMEDIATE , ADD IF NOT EXISTS column_name data_type CONSTRAINT constraint_name REFERENCES reftable MATCH PARTIAL ON UPDATE SET DEFAULT ( column_name , column_name ) NOT DEFERRABLE CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH SIMPLE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE"), - Parses("ALTER TABLE IF EXISTS name ADD COLUMN IF NOT EXISTS column_name data_type COLLATE en_US CONSTRAINT constraint_name NOT NULL NOT DEFERRABLE INITIALLY IMMEDIATE CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH FULL ON DELETE SET DEFAULT ON UPDATE CASCADE NOT DEFERRABLE INITIALLY DEFERRED , ADD COLUMN column_name data_type REFERENCES reftable ON DELETE SET NULL ( column_name ) ON UPDATE SET NULL ( column_name ) INITIALLY IMMEDIATE CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH SIMPLE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE"), + Converts("ALTER TABLE IF EXISTS name ADD COLUMN IF NOT EXISTS column_name data_type COLLATE en_US CONSTRAINT constraint_name NOT NULL NOT DEFERRABLE INITIALLY IMMEDIATE CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH FULL ON DELETE SET DEFAULT ON UPDATE CASCADE NOT DEFERRABLE INITIALLY DEFERRED , ADD COLUMN column_name data_type REFERENCES reftable ON DELETE SET NULL ( column_name ) ON UPDATE SET NULL ( column_name ) INITIALLY IMMEDIATE CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH SIMPLE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE"), Parses("ALTER TABLE IF EXISTS name * ADD COLUMN IF NOT EXISTS column_name data_type CONSTRAINT constraint_name REFERENCES reftable MATCH PARTIAL ON DELETE SET DEFAULT ( column_name ) ON UPDATE SET NULL ( column_name ) DEFERRABLE INITIALLY DEFERRED REFERENCES reftable ( refcolumn ) ON DELETE CASCADE ON UPDATE SET DEFAULT ( column_name ) , ADD IF NOT EXISTS column_name data_type COLLATE en_US CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH PARTIAL ON UPDATE NO ACTION CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) ON DELETE NO ACTION ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE"), Converts("ALTER TABLE IF EXISTS ONLY name ADD IF NOT EXISTS column_name data_type COLLATE en_US CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH FULL ON DELETE SET NULL ( column_name ) ON UPDATE SET DEFAULT ( column_name , column_name ) DEFERRABLE INITIALLY IMMEDIATE REFERENCES reftable ( refcolumn ) MATCH FULL ON DELETE CASCADE ON UPDATE NO ACTION INITIALLY IMMEDIATE , ADD column_name data_type COLLATE en_US REFERENCES reftable ON DELETE SET DEFAULT ( column_name ) ON UPDATE RESTRICT DEFERRABLE CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) ON DELETE NO ACTION ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE"), Parses("ALTER TABLE name ADD COLUMN column_name data_type CONSTRAINT constraint_name REFERENCES reftable ( refcolumn ) MATCH SIMPLE ON DELETE NO ACTION ON UPDATE RESTRICT NOT DEFERRABLE INITIALLY DEFERRED REFERENCES reftable MATCH PARTIAL ON DELETE SET DEFAULT ( column_name , column_name ) NOT DEFERRABLE INITIALLY DEFERRED , ADD COLUMN IF NOT EXISTS column_name data_type COLLATE en_US REFERENCES reftable ( refcolumn ) MATCH FULL ON DELETE SET DEFAULT ( column_name , column_name ) ON UPDATE NO ACTION INITIALLY DEFERRED CONSTRAINT constraint_name REFERENCES reftable MATCH FULL ON DELETE NO ACTION ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE"), diff --git a/testing/go/alter_table_test.go b/testing/go/alter_table_test.go index 06134ba0ed..03fa5ccc15 100644 --- a/testing/go/alter_table_test.go +++ b/testing/go/alter_table_test.go @@ -1658,7 +1658,7 @@ JOIN pg_catalog.pg_namespace ns ON cl.relnamespace = ns.oid WHERE con.contype = 'c' ORDER BY schema_name, table_name;`, // TODO: the check should `CHECK ((b > 10))` - Expected: []sql.Row{{"public", "attmp3", "b_greater_than_ten", `b_greater_than_ten CHECK "b" > 10 ENFORCED`}}, + Expected: []sql.Row{{"public", "attmp3", "b_greater_than_ten", `b_greater_than_ten CHECK ("b" > 10) ENFORCED`}}, }, }, }, diff --git a/testing/go/dolt_tables_test.go b/testing/go/dolt_tables_test.go index fc95158732..fab3a4e23d 100644 --- a/testing/go/dolt_tables_test.go +++ b/testing/go/dolt_tables_test.go @@ -1480,7 +1480,7 @@ func TestUserSpaceDoltTables(t *testing.T) { {"Project"}, {" ├─ columns: [dolt_diff_test.to_id]"}, {" └─ Filter"}, - {" ├─ to_commit = '0123456789abcdefghij0123456789ab'"}, + {" ├─ (to_commit = '0123456789abcdefghij0123456789ab')"}, {" └─ IndexedTableAccess(dolt_diff_test)"}, {" ├─ index: [dolt_diff_test.to_commit]"}, {" └─ filters: [{[0123456789abcdefghij0123456789ab, 0123456789abcdefghij0123456789ab]}]"}, @@ -1493,7 +1493,7 @@ func TestUserSpaceDoltTables(t *testing.T) { {"Project"}, {" ├─ columns: [dolt_diff_test.to_id]"}, {" └─ Filter"}, - {" ├─ (from_commit = '0123456789abcdefghij0123456789ab' AND to_commit = 'ab0123456789abcdefghij0123456789')"}, + {" ├─ ((from_commit = '0123456789abcdefghij0123456789ab') AND (to_commit = 'ab0123456789abcdefghij0123456789'))"}, {" └─ IndexedTableAccess(dolt_diff_test)"}, {" ├─ index: [dolt_diff_test.from_commit]"}, {" └─ filters: [{[0123456789abcdefghij0123456789ab, 0123456789abcdefghij0123456789ab]}]"}, @@ -1509,7 +1509,7 @@ func TestUserSpaceDoltTables(t *testing.T) { {"Project"}, {" ├─ columns: [dolt_diff_test.to_id]"}, {" └─ Filter"}, - {" ├─ to_commit < '0123456789abcdefghij0123456789ab'"}, + {" ├─ (to_commit < '0123456789abcdefghij0123456789ab')"}, {" └─ Table"}, {" └─ name: dolt_diff_test"}, }, @@ -1520,7 +1520,7 @@ func TestUserSpaceDoltTables(t *testing.T) { {"Project"}, {" ├─ columns: [dolt_diff_test.to_id]"}, {" └─ Filter"}, - {" ├─ to_commit >= '0123456789abcdefghij0123456789ab'"}, + {" ├─ (to_commit >= '0123456789abcdefghij0123456789ab')"}, {" └─ Table"}, {" └─ name: dolt_diff_test"}, }, diff --git a/testing/go/enginetest/doltgres_harness_test.go b/testing/go/enginetest/doltgres_harness_test.go index 71b8d8d522..54c8372aed 100644 --- a/testing/go/enginetest/doltgres_harness_test.go +++ b/testing/go/enginetest/doltgres_harness_test.go @@ -482,8 +482,6 @@ var showCreateTableQueryRegex = regexp.MustCompile(`(?i)^\s*show\s+create\s+tabl // - The trailing `) ENGINE=InnoDB DEFAULT CHARSET=… COLLATE=…` clause // becomes a bare `)`. // - The MySQL `DEFAULT CURRENT_TIMESTAMP` form becomes `DEFAULT (now())`. -// - Doubled parentheses around a DEFAULT expression `((expr))` become -// `(expr)` (MySQL's `((7 + 11))` vs postgres' `(7 + 11)`). // - A trailing comma left dangling after we drop a KEY clause is removed. func convertShowCreateTableExpected(t *testing.T, q string, expected []sql.Row) bool { if !showCreateTableQueryRegex.MatchString(q) { @@ -512,10 +510,6 @@ var reUniqueKey = regexp.MustCompile("(?m)^(\\s*)UNIQUE KEY `([^`]+)` \\(([^)]+) // reKeyLine matches a non-unique `KEY \`name\` (cols)` line. var reKeyLine = regexp.MustCompile("(?m)^\\s*KEY `[^`]+` \\([^)]+\\),?\n") -// reDoubleParenDefault matches a DEFAULT clause wrapped in two layers of -// parentheses, like `DEFAULT ((7 + 11))`. -var reDoubleParenDefault = regexp.MustCompile(`DEFAULT \(\(([^()]+)\)\)`) - // reBacktickIdent matches a backtick-quoted MySQL identifier — we replace // these with double-quoted postgres identifiers. var reBacktickIdent = regexp.MustCompile("`([^`]*)`") @@ -573,7 +567,6 @@ func translateMysqlShowCreateTable(s string) string { // 5. DEFAULT CURRENT_TIMESTAMP → DEFAULT (now()) and unwrap doubled parens. s = strings.ReplaceAll(s, "DEFAULT CURRENT_TIMESTAMP", "DEFAULT (now())") - s = reDoubleParenDefault.ReplaceAllString(s, "DEFAULT ($1)") // 6. Backticks → double quotes. s = reBacktickIdent.ReplaceAllString(s, `"$1"`) diff --git a/testing/go/enginetest/query_converter_test.go b/testing/go/enginetest/query_converter_test.go index eeb9e75d0e..85c743b4fd 100644 --- a/testing/go/enginetest/query_converter_test.go +++ b/testing/go/enginetest/query_converter_test.go @@ -2220,14 +2220,14 @@ func TestTranslateMysqlShowCreateTable(t *testing.T) { ")", }, { - name: "DEFAULT CURRENT_TIMESTAMP and doubled parens", + name: "DEFAULT CURRENT_TIMESTAMP", input: "CREATE TABLE `t` (\n" + " `a` int DEFAULT CURRENT_TIMESTAMP,\n" + " `b` int NOT NULL DEFAULT ((7 + 11))\n" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin", expected: "CREATE TABLE \"t\" (\n" + " \"a\" integer DEFAULT (now()),\n" + - " \"b\" integer NOT NULL DEFAULT (7 + 11)\n" + + " \"b\" integer NOT NULL DEFAULT ((7 + 11))\n" + ")", }, { diff --git a/testing/go/extensions/pgvector_knn_test.go b/testing/go/extensions/pgvector_knn_test.go index 1eac833600..055de3635f 100644 --- a/testing/go/extensions/pgvector_knn_test.go +++ b/testing/go/extensions/pgvector_knn_test.go @@ -79,7 +79,7 @@ func TestPgvectorKnn(t *testing.T) { {" ├─ columns: [knn.id]"}, {" └─ IndexedTableAccess(knn)"}, {" ├─ index: [knn.v]"}, - {" ├─ order: knn.v <-> '[1.5,1,2]' LIMIT 4 (bigint)"}, + {" ├─ order: (knn.v <-> '[1.5,1,2]') LIMIT 4 (bigint)"}, {" └─ columns: [id v]"}, }, }, @@ -134,7 +134,7 @@ func TestPgvectorKnn(t *testing.T) { {" ├─ columns: [knn.id]"}, {" └─ IndexedTableAccess(knn)"}, {" ├─ index: [knn.h]"}, - {" ├─ order: knn.h <-> '[1.5,1,2]' LIMIT 4 (bigint)"}, + {" ├─ order: (knn.h <-> '[1.5,1,2]') LIMIT 4 (bigint)"}, {" └─ columns: [id h]"}, }, }, @@ -170,7 +170,7 @@ func TestPgvectorKnn(t *testing.T) { {" ├─ columns: [knn.id]"}, {" └─ IndexedTableAccess(knn)"}, {" ├─ index: [knn.v]"}, - {" ├─ order: knn.v <-> '[1.5,1,2]' LIMIT 4 (bigint)"}, + {" ├─ order: (knn.v <-> '[1.5,1,2]') LIMIT 4 (bigint)"}, {" └─ columns: [id v]"}, }, }, diff --git a/testing/go/functional_index_multi_expr_test.go b/testing/go/functional_index_multi_expr_test.go index fb89f6b308..fa927f70d5 100644 --- a/testing/go/functional_index_multi_expr_test.go +++ b/testing/go/functional_index_multi_expr_test.go @@ -43,11 +43,11 @@ func TestFunctionalIndexMultiExpr(t *testing.T) { }, { Query: "SELECT pg_get_indexdef('idx1'::regclass);", - Expected: []sql.Row{{"CREATE INDEX idx1 ON public.t USING btree ((upper(name)), age, (c1 + c2))"}}, + Expected: []sql.Row{{"CREATE INDEX idx1 ON public.t USING btree ((upper(name)), age, ((c1 + c2)))"}}, }, { Query: "SELECT indexdef FROM pg_indexes WHERE indexname = 'idx1';", - Expected: []sql.Row{{"CREATE INDEX idx1 ON public.t USING btree ((upper(name)), age, (c1 + c2))"}}, + Expected: []sql.Row{{"CREATE INDEX idx1 ON public.t USING btree ((upper(name)), age, ((c1 + c2)))"}}, }, }, }, diff --git a/testing/go/index_test.go b/testing/go/index_test.go index fa8cdccde5..e6f4192037 100644 --- a/testing/go/index_test.go +++ b/testing/go/index_test.go @@ -285,9 +285,9 @@ func TestBasicIndexing(t *testing.T) { Query: "explain select * from test join jointable on test.v1 = jointable.v3 and test.v2 = 22 order by 1", Expected: []sql.Row{ {"InnerJoin"}, - {" ├─ test.v1 = jointable.v3"}, + {" ├─ (test.v1 = jointable.v3)"}, {" ├─ Filter"}, - {" │ ├─ test.v2 = 22"}, + {" │ ├─ (test.v2 = 22)"}, {" │ └─ IndexedTableAccess(test)"}, {" │ ├─ index: [test.pk]"}, {" │ ├─ filters: [{[NULL, ∞)}]"}, @@ -307,7 +307,7 @@ func TestBasicIndexing(t *testing.T) { Query: "explain select * from test join jointable on test.v1 = jointable.v3 and test.v2 = jointable.v4 order by 1", Expected: []sql.Row{ {"InnerJoin"}, - {" ├─ (test.v1 = jointable.v3 AND test.v2 = jointable.v4)"}, + {" ├─ ((test.v1 = jointable.v3) AND (test.v2 = jointable.v4))"}, {" ├─ IndexedTableAccess(test)"}, {" │ ├─ index: [test.pk]"}, {" │ ├─ filters: [{[NULL, ∞)}]"}, @@ -464,9 +464,9 @@ func TestBasicIndexing(t *testing.T) { Query: "explain select * from test join jointable on test.v1 = jointable.v3 and test.v2 = 22 order by 1", Expected: []sql.Row{ {"InnerJoin"}, - {" ├─ test.v1 = jointable.v3"}, + {" ├─ (test.v1 = jointable.v3)"}, {" ├─ Filter"}, - {" │ ├─ test.v2 = 22"}, + {" │ ├─ (test.v2 = 22)"}, {" │ └─ IndexedTableAccess(test)"}, {" │ ├─ index: [test.pk]"}, {" │ ├─ filters: [{[NULL, ∞)}]"}, @@ -500,7 +500,7 @@ func TestBasicIndexing(t *testing.T) { Query: "explain select * from test join (select * from jointable) sq on test.v1 = sq.v3 and test.v2 = sq.v4 order by 1", Expected: []sql.Row{ {"InnerJoin"}, - {" ├─ (test.v1 = sq.v3 AND test.v2 = sq.v4)"}, + {" ├─ ((test.v1 = sq.v3) AND (test.v2 = sq.v4))"}, {" ├─ IndexedTableAccess(test)"}, {" │ ├─ index: [test.pk]"}, {" │ ├─ filters: [{[NULL, ∞)}]"}, @@ -1497,7 +1497,7 @@ func TestBasicIndexing(t *testing.T) { Query: "EXPLAIN SELECT * FROM t WHERE v1 = 20;", Expected: []sql.Row{ {"Filter"}, - {" ├─ t.v1 = 20"}, + {" ├─ (t.v1 = 20)"}, {" └─ Table"}, {" ├─ name: t"}, {" └─ columns: [pk v1]"}, @@ -1675,7 +1675,7 @@ func TestBasicIndexing(t *testing.T) { }, { Query: "SELECT indexdef FROM pg_indexes WHERE indexname = 'idx_one_active_session_per_user';", - Expected: []sql.Row{{"CREATE UNIQUE INDEX idx_one_active_session_per_user ON public.user_sessions USING btree (user_id) WHERE (user_sessions.is_active = true)"}}, + Expected: []sql.Row{{"CREATE UNIQUE INDEX idx_one_active_session_per_user ON public.user_sessions USING btree (user_id) WHERE ((user_sessions.is_active = true))"}}, }, { Query: "INSERT INTO user_sessions (user_id, is_active) VALUES (42, true);", @@ -1734,13 +1734,13 @@ func TestBasicIndexing(t *testing.T) { }, { Query: "SELECT indexdef FROM pg_indexes WHERE indexname = 'idx_partial';", - Expected: []sql.Row{{"CREATE INDEX idx_partial ON public.t USING btree (a) WHERE (t.a > 1)"}}, + Expected: []sql.Row{{"CREATE INDEX idx_partial ON public.t USING btree (a) WHERE ((t.a > 1))"}}, }, { Query: "EXPLAIN SELECT * FROM t WHERE a > 1;", Expected: []sql.Row{ {"IndexedTableAccess(t)"}, - {" ├─ index: [t.a,t.a > 1]"}, + {" ├─ index: [t.a,(t.a > 1)]"}, {" ├─ filters: [{(1, ∞)}]"}, {" └─ columns: [a b]"}, }, @@ -1749,7 +1749,7 @@ func TestBasicIndexing(t *testing.T) { Query: "EXPLAIN SELECT * FROM t WHERE a > 0;", Expected: []sql.Row{ {"Filter"}, - {" ├─ t.a > 0"}, + {" ├─ (t.a > 0)"}, {" └─ Table"}, {" ├─ name: t"}, {" └─ columns: [a b]"}, diff --git a/testing/go/issues_test.go b/testing/go/issues_test.go index e410763ed0..d0b0bf6ad6 100644 --- a/testing/go/issues_test.go +++ b/testing/go/issues_test.go @@ -617,6 +617,316 @@ FROM pg_constraint c JOIN pg_class cl ON c.conrelid = cl.oid WHERE cl.relname = }, }, }, + { + Name: "Issue #3323: INSERT after ALTER TABLE ADD COLUMN on a table with a generated column", + SetUpScript: []string{ + "CREATE TABLE t3323 (a INT PRIMARY KEY, b INT GENERATED ALWAYS AS (a + 1) STORED);", + "INSERT INTO t3323 (a) VALUES (1);", + "ALTER TABLE t3323 ADD COLUMN c INT DEFAULT 0;", + "CREATE TABLE t3323b (a INT PRIMARY KEY, b TEXT GENERATED ALWAYS AS (upper(a::text)) STORED);", + "INSERT INTO t3323b (a) VALUES (1);", + "ALTER TABLE t3323b ADD COLUMN c INT DEFAULT 0;", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "INSERT INTO t3323 (a) VALUES (2);", + Expected: []sql.Row{}, + }, + { + Query: "SELECT * FROM t3323 ORDER BY a;", + Expected: []sql.Row{{1, 2, 0}, {2, 3, 0}}, + }, + { + Query: "INSERT INTO t3323b (a) VALUES (2);", + Expected: []sql.Row{}, + }, + { + Query: "SELECT * FROM t3323b ORDER BY a;", + Expected: []sql.Row{{1, "1", 0}, {2, "2", 0}}, + }, + }, + }, + { + Name: "Issue #3324: parentheses are kept in stored default, generated, and check expressions", + SetUpScript: []string{ + "CREATE TABLE t3324 (a INT PRIMARY KEY, b INT DEFAULT (1 + 1) * 2, c INT DEFAULT 2 * (3 + 1) + 1, d INT DEFAULT -(1 + 1), e INT GENERATED ALWAYS AS ((a + 1) * 2) STORED, CONSTRAINT chk3324 CHECK (((a + 1) * 2) > 3));", + "INSERT INTO t3324 (a) VALUES (1);", + "ALTER TABLE t3324 ADD COLUMN f INT DEFAULT (1 + 1) * 2;", + "INSERT INTO t3324 (a) VALUES (2);", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT * FROM t3324 ORDER BY a;", + Expected: []sql.Row{{1, 4, 9, -2, 4, 4}, {2, 4, 9, -2, 6, 4}}, + }, + { + Query: "SELECT column_name, column_default FROM information_schema.columns WHERE table_name = 't3324' AND column_default IS NOT NULL ORDER BY ordinal_position;", + Expected: []sql.Row{{"b", "((1 + 1) * 2)"}, {"c", "((2 * (3 + 1)) + 1)"}, {"d", "(-(1 + 1))"}, {"f", "((1 + 1) * 2)"}}, + }, + { + Query: "SELECT check_clause FROM information_schema.check_constraints WHERE constraint_name = 'chk3324';", + Expected: []sql.Row{{`((("a" + 1) * 2) > 3)`}}, + }, + }, + }, + { + Name: "Issue #3325: trailing spaces of a bpchar value are ignored", + SetUpScript: []string{ + "CREATE TABLE t3325 (id INT PRIMARY KEY, c CHAR(2) CHECK (c::text IN ('L', 'R')));", + "INSERT INTO t3325 VALUES (1, 'L'), (2, 'R ');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT '[' || 'L'::CHAR(2) || ']', length('L'::CHAR(2)), 'L'::CHAR(2) = 'L', 'L '::CHAR(2) = 'L'::CHAR(2), 'L'::CHAR(2)::TEXT = 'L', 'L'::CHAR(2)::VARCHAR = 'L', bpcharcmp('L'::CHAR(2), 'L ');", + Expected: []sql.Row{{"[L]", 1, "t", "t", "t", "t", 0}}, + }, + { + Query: "SELECT id, '[' || c || ']', c = 'L' FROM t3325 ORDER BY id;", + Expected: []sql.Row{{1, "[L]", "t"}, {2, "[R]", "f"}}, + }, + }, + }, + { + Name: "Issue #3326: convert_from and decode", + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT convert_from('\x68656c6c6f'::BYTEA, 'UTF8'), convert_from(decode('68656c6c6f', 'hex'), 'UTF8'), convert_from('\xc3a9'::BYTEA, 'UTF8'), convert_from('\xe9'::BYTEA, 'LATIN1');`, + Expected: []sql.Row{{"hello", "hello", "é", "é"}}, + }, + { + Query: `SELECT decode('aGVsbG8=', 'base64'), decode('abc\000', 'escape'), decode('a\\b', 'escape'), decode('68 65', 'hex');`, + Expected: []sql.Row{{[]byte("hello"), []byte{0x61, 0x62, 0x63, 0x00}, []byte(`a\b`), []byte("he")}}, + }, + { + Query: `SELECT convert_from('\xff'::BYTEA, 'UTF8');`, + ExpectedErr: `invalid byte sequence for encoding "UTF8": 0xff`, + }, + { + Query: `SELECT convert_from('\x68'::BYTEA, 'NOPE');`, + ExpectedErr: `invalid source encoding name "NOPE"`, + }, + { + Query: "SELECT decode('6', 'hex');", + ExpectedErr: "invalid hexadecimal data: odd number of digits", + }, + { + Query: "SELECT decode('6g', 'hex');", + ExpectedErr: `invalid hexadecimal digit: "g"`, + }, + { + Query: "SELECT decode('abc', 'nope');", + ExpectedErr: `unrecognized encoding: "nope"`, + }, + }, + }, + { + Name: "Issue #3327: roles can execute routines without an explicit grant", + SetUpScript: []string{ + "CREATE TABLE t3327 (x INT);", + "INSERT INTO t3327 VALUES (1), (2), (3);", + "CREATE ROLE reader LOGIN PASSWORD 'password';", + "GRANT SELECT ON t3327 TO reader;", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT COUNT(*), SUM(x), MAX(x) FROM t3327;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{3, 6, 3}}, + }, + { + Query: "SELECT x, ROW_NUMBER() OVER (ORDER BY x) FROM t3327 ORDER BY x;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{1, 1}, {2, 2}, {3, 3}}, + }, + { + Query: "SELECT pg_catalog.lower('A'), lower('A');", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"a", "a"}}, + }, + }, + }, + { + Name: "Issue #3328: information_schema.columns.generation_expression", + SetUpScript: []string{ + "CREATE TABLE t3328 (a INT PRIMARY KEY, s TEXT, b INT GENERATED ALWAYS AS (a + 1) STORED, c TEXT GENERATED ALWAYS AS (upper(s)) STORED, d INT GENERATED ALWAYS AS ((a + 1) * 2) STORED, e INT GENERATED ALWAYS AS (a) STORED);", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT column_name, is_generated, generation_expression FROM information_schema.columns WHERE table_name = 't3328' ORDER BY ordinal_position;", + Expected: []sql.Row{ + {"a", "NEVER", nil}, + {"s", "NEVER", nil}, + {"b", "ALWAYS", `"a" + 1`}, + {"c", "ALWAYS", `upper("s")`}, + {"d", "ALWAYS", `("a" + 1) * 2`}, + {"e", "ALWAYS", `"a"`}, + }, + }, + }, + }, + { + Name: "Issue #3330: information_schema.triggers", + SetUpScript: []string{ + "CREATE TABLE t3330 (a INT PRIMARY KEY);", + "CREATE FUNCTION f3330() RETURNS TRIGGER AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;", + "CREATE TRIGGER tr_b BEFORE INSERT OR UPDATE ON t3330 FOR EACH ROW EXECUTE FUNCTION f3330();", + "CREATE TRIGGER tr_a AFTER INSERT ON t3330 FOR EACH ROW EXECUTE FUNCTION f3330('x', 'y');", + "CREATE TRIGGER tr_c BEFORE INSERT ON t3330 FOR EACH ROW EXECUTE FUNCTION f3330();", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT trigger_catalog, trigger_schema, trigger_name, event_manipulation, event_object_table, action_order, action_condition, action_statement, action_orientation, action_timing FROM information_schema.triggers ORDER BY trigger_name, event_manipulation;", + Expected: []sql.Row{ + {"postgres", "public", "tr_a", "INSERT", "t3330", 1, nil, "EXECUTE FUNCTION f3330('x', 'y')", "ROW", "AFTER"}, + {"postgres", "public", "tr_b", "INSERT", "t3330", 1, nil, "EXECUTE FUNCTION f3330()", "ROW", "BEFORE"}, + {"postgres", "public", "tr_b", "UPDATE", "t3330", 1, nil, "EXECUTE FUNCTION f3330()", "ROW", "BEFORE"}, + {"postgres", "public", "tr_c", "INSERT", "t3330", 2, nil, "EXECUTE FUNCTION f3330()", "ROW", "BEFORE"}, + }, + }, + }, + }, + { + Name: "Issue #3332: named column constraints", + SetUpScript: []string{ + "CREATE TABLE t3332 (id INT CONSTRAINT id_nn NOT NULL, u INT CONSTRAINT u_uni UNIQUE, d INT CONSTRAINT d_def DEFAULT 5, n INT CONSTRAINT n_null NULL, PRIMARY KEY (id));", + "ALTER TABLE t3332 ADD COLUMN w INT CONSTRAINT w_nn NOT NULL DEFAULT 1 CONSTRAINT w_uni UNIQUE;", + "INSERT INTO t3332 (id, u) VALUES (1, 1);", + "INSERT INTO t3332 (id, u, w) VALUES (2, 2, 2);", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT * FROM t3332 ORDER BY id;", + Expected: []sql.Row{{1, 1, 5, nil, 1}, {2, 2, 5, nil, 2}}, + }, + { + Query: "SELECT indexname FROM pg_indexes WHERE tablename = 't3332' ORDER BY indexname;", + Expected: []sql.Row{{"t3332_pkey"}, {"u_uni"}, {"w_uni"}}, + }, + { + Query: "INSERT INTO t3332 (id, u, w) VALUES (3, 1, 3);", + ExpectedErr: "duplicate unique key", + }, + { + Query: "INSERT INTO t3332 (id, u, w) VALUES (3, 3, 2);", + ExpectedErr: "duplicate unique key", + }, + { + Query: "INSERT INTO t3332 (id, u, w) VALUES (NULL, 4, 4);", + ExpectedErr: "non-nullable", + }, + }, + }, + { + Name: "Issue #3333: CHECK constraint calling a function", + SetUpScript: []string{ + "CREATE TABLE t3333 (z TEXT PRIMARY KEY CHECK (z ~ '^[0-9]+$'), y TEXT CONSTRAINT y_chk CHECK (regexp_like(y, '^[a-z]+$')));", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "INSERT INTO t3333 VALUES ('123', 'abc');", + Expected: []sql.Row{}, + }, + { + Query: "INSERT INTO t3333 VALUES ('12a', 'abc');", + ExpectedErr: "violated", + }, + { + Query: "INSERT INTO t3333 VALUES ('124', 'ABC');", + ExpectedErr: `Check constraint "y_chk" violated`, + }, + { + Query: "SELECT check_clause FROM information_schema.check_constraints WHERE constraint_name = 'y_chk';", + Expected: []sql.Row{{`regexp_like("y",'^[a-z]+$')`}}, + }, + { + Query: "SELECT * FROM t3333;", + Expected: []sql.Row{{"123", "abc"}}, + }, + }, + }, + { + Name: "Issue #3334: regnamespace", + SetUpScript: []string{ + "CREATE SCHEMA s3334;", + "CREATE TABLE t3334 (id INT PRIMARY KEY, ns REGNAMESPACE);", + "INSERT INTO t3334 VALUES (1, 'public'), (2, 's3334');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT 'public'::REGNAMESPACE, 'public'::REGNAMESPACE::OID = (SELECT oid FROM pg_namespace WHERE nspname = 'public'), 's3334'::REGNAMESPACE::TEXT, to_regnamespace('s3334') IS NOT NULL, to_regnamespace('nope'), (SELECT nspname FROM pg_namespace WHERE oid = 's3334'::REGNAMESPACE);", + Expected: []sql.Row{{"public", "t", "s3334", "t", nil, "s3334"}}, + }, + { + Query: "SELECT relnamespace::REGNAMESPACE FROM pg_class WHERE relname = 't3334';", + Expected: []sql.Row{{"public"}}, + }, + { + Query: "SELECT typname FROM pg_type WHERE typname = 'regnamespace';", + Expected: []sql.Row{{"regnamespace"}}, + }, + { + Query: "SELECT id, ns, ns::TEXT FROM t3334 ORDER BY id;", + Expected: []sql.Row{{1, "public", "public"}, {2, "s3334", "s3334"}}, + }, + { + Query: "SELECT 'nope'::REGNAMESPACE;", + ExpectedErr: `schema "nope" does not exist`, + }, + { + Query: "SELECT 'a.b'::REGNAMESPACE;", + ExpectedErr: "invalid name syntax", + }, + }, + }, + { + Name: "Issue #3336: OLD.* IS DISTINCT FROM NEW.* in a trigger", + SetUpScript: []string{ + "CREATE TABLE t3336 (a INT PRIMARY KEY, b TEXT);", + "CREATE TABLE t3336_log (a INT, src TEXT);", + "CREATE FUNCTION f3336_when() RETURNS TRIGGER AS $$ BEGIN INSERT INTO t3336_log VALUES (NEW.a, 'when'); RETURN NEW; END; $$ LANGUAGE plpgsql;", + "CREATE FUNCTION f3336_body() RETURNS TRIGGER AS $$ BEGIN IF OLD.* IS DISTINCT FROM NEW.* THEN INSERT INTO t3336_log VALUES (NEW.a, 'body'); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql;", + "CREATE TRIGGER tr3336_when AFTER UPDATE ON t3336 FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*) EXECUTE FUNCTION f3336_when();", + "CREATE TRIGGER tr3336_body AFTER UPDATE ON t3336 FOR EACH ROW EXECUTE FUNCTION f3336_body();", + "INSERT INTO t3336 VALUES (1, 'x'), (2, NULL);", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT ROW(2, NULL::TEXT) IS DISTINCT FROM ROW(2, 'y'::TEXT), ROW(2, NULL::TEXT) IS DISTINCT FROM ROW(2, NULL::TEXT), ROW(2, NULL::TEXT) IS NOT DISTINCT FROM ROW(2, NULL::TEXT), ROW(1, 2) IS DISTINCT FROM ROW(1, 3), ROW(1, 2) IS NOT DISTINCT FROM ROW(1, 2);", + Expected: []sql.Row{{"t", "f", "t", "t", "t"}}, + }, + { + Query: "UPDATE t3336 SET b = 'x' WHERE a = 1;", + Expected: []sql.Row{}, + }, + { + Query: "SELECT COUNT(*) FROM t3336_log;", + Expected: []sql.Row{{0}}, + }, + { + Query: "UPDATE t3336 SET b = 'y' WHERE a = 2;", + Expected: []sql.Row{}, + }, + { + Query: "UPDATE t3336 SET b = NULL WHERE a = 2;", + Expected: []sql.Row{}, + }, + { + Query: "UPDATE t3336 SET b = NULL WHERE a = 2;", + Expected: []sql.Row{}, + }, + { + Query: "UPDATE t3336 SET b = 'z' WHERE a = 1;", + Expected: []sql.Row{}, + }, + { + Query: "SELECT * FROM t3336_log ORDER BY a, src;", + Expected: []sql.Row{{1, "body"}, {1, "when"}, {2, "body"}, {2, "body"}, {2, "when"}, {2, "when"}}, + }, + }, + }, }) } diff --git a/testing/go/pgcatalog_test.go b/testing/go/pgcatalog_test.go index c58557d057..033aa0c240 100644 --- a/testing/go/pgcatalog_test.go +++ b/testing/go/pgcatalog_test.go @@ -571,7 +571,7 @@ func TestPgCast(t *testing.T) { Assertions: []ScriptTestAssertion{ { Query: `SELECT COUNT(*) FROM "pg_catalog"."pg_cast";`, - Expected: []sql.Row{{118}}, + Expected: []sql.Row{{125}}, }, { // Different cases and quoted, so it fails Query: `SELECT * FROM "PG_catalog"."pg_cast";`, @@ -583,7 +583,7 @@ func TestPgCast(t *testing.T) { }, { // Different cases but non-quoted, so it works Query: "SELECT COUNT(*) FROM PG_catalog.pg_CAST ORDER BY oid;", - Expected: []sql.Row{{118}}, + Expected: []sql.Row{{125}}, }, }, }, @@ -894,7 +894,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" ├─ columns: [pg_constraint.oid, pg_constraint.conname]"}, {" └─ Sort(pg_constraint.oid ASC)"}, {" └─ Filter"}, - {" ├─ pg_constraint.oid = 2068729390"}, + {" ├─ (pg_constraint.oid = 2068729390)"}, {" └─ IndexedTableAccess(pg_constraint)"}, {" ├─ index: [pg_constraint.oid]"}, {" └─ filters: [{[{Index:[\"public\",\"testing2\",\"PRIMARY\"]}, {Index:[\"public\",\"testing2\",\"PRIMARY\"]}]}]"}, @@ -966,7 +966,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" ├─ columns: [pg_constraint.conname, pg_constraint.connamespace]"}, {" └─ Sort(pg_constraint.conname ASC)"}, {" └─ Filter"}, - {" ├─ (pg_constraint.conname = 'test_table1_pkey' AND pg_constraint.connamespace = 2200)"}, + {" ├─ ((pg_constraint.conname = 'test_table1_pkey') AND (pg_constraint.connamespace = 2200))"}, {" └─ IndexedTableAccess(pg_constraint)"}, {" ├─ index: [pg_constraint.conname,pg_constraint.connamespace]"}, {" └─ filters: [{[test_table1_pkey, test_table1_pkey], [{Namespace:[\"public\"]}, {Namespace:[\"public\"]}]}]"}, @@ -1002,7 +1002,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" ├─ columns: [pg_constraint.conname]"}, {" └─ Sort(pg_constraint.conname ASC)"}, {" └─ Filter"}, - {" ├─ (pg_constraint.conrelid = 3645786842 AND pg_constraint.contypid = 0)"}, + {" ├─ ((pg_constraint.conrelid = 3645786842) AND (pg_constraint.contypid = 0))"}, {" └─ IndexedTableAccess(pg_constraint)"}, {" ├─ index: [pg_constraint.conrelid,pg_constraint.contypid,pg_constraint.conname]"}, {" └─ filters: [{[{Table:[\"public\",\"test_table1\"]}, {Table:[\"public\",\"test_table1\"]}], [{OID:[\"0\"]}, {OID:[\"0\"]}], [NULL, ∞)}]"}, @@ -1055,7 +1055,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" ├─ columns: [pg_constraint.conname]"}, {" └─ Sort(pg_constraint.conname ASC)"}, {" └─ Filter"}, - {" ├─ pg_constraint.contypid = 1309307140"}, + {" ├─ (pg_constraint.contypid = 1309307140)"}, {" └─ IndexedTableAccess(pg_constraint)"}, {" ├─ index: [pg_constraint.contypid]"}, {" └─ filters: [{[{Type:[\"public\",\"test_domain\"]}, {Type:[\"public\",\"test_domain\"]}]}]"}, @@ -1095,7 +1095,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" ├─ columns: [pg_constraint.conname]"}, {" └─ Sort(pg_constraint.conname ASC)"}, {" └─ Filter"}, - {" ├─ ((pg_constraint.conrelid >= Subquery"}, + {" ├─ (((pg_constraint.conrelid >= Subquery"}, {" │ ├─ cacheable: true"}, {" │ └─ Project"}, {" │ ├─ columns: [min(pg_class.oid) as `min`]"}, @@ -1106,7 +1106,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" │ ├─ pg_class.relname LIKE 'test_%'"}, {" │ └─ Table"}, {" │ └─ name: pg_class"}, - {" │ AND pg_constraint.conrelid <= Subquery"}, + {" │ ) AND (pg_constraint.conrelid <= Subquery"}, {" │ ├─ cacheable: true"}, {" │ └─ Project"}, {" │ ├─ columns: [max(pg_class.oid) as `max`]"}, @@ -1117,7 +1117,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" │ ├─ pg_class.relname LIKE 'test_%'"}, {" │ └─ Table"}, {" │ └─ name: pg_class"}, - {" │ ) AND pg_constraint.contypid = 0)"}, + {" │ )) AND (pg_constraint.contypid = 0))"}, {" └─ IndexedTableAccess(pg_constraint)"}, {" ├─ index: [pg_constraint.contypid]"}, {" └─ filters: [{[{OID:[\"0\"]}, {OID:[\"0\"]}]}]"}, @@ -1147,7 +1147,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" ├─ columns: [pg_constraint.conname]"}, {" └─ Sort(pg_constraint.conname ASC)"}, {" └─ Filter"}, - {" ├─ pg_constraint.conrelid = 3645786842"}, + {" ├─ (pg_constraint.conrelid = 3645786842)"}, {" └─ IndexedTableAccess(pg_constraint)"}, {" ├─ index: [pg_constraint.conrelid,pg_constraint.contypid,pg_constraint.conname]"}, {" └─ filters: [{[{Table:[\"public\",\"test_table1\"]}, {Table:[\"public\",\"test_table1\"]}], [NULL, ∞), [NULL, ∞)}]"}, @@ -1170,7 +1170,7 @@ func TestPgConstraintIndexes(t *testing.T) { {"Project"}, {" ├─ columns: [pg_constraint.conname]"}, {" └─ Filter"}, - {" ├─ (pg_constraint.conname = 'test_table1_pkey' AND pg_constraint.connamespace = 2200)"}, + {" ├─ ((pg_constraint.conname = 'test_table1_pkey') AND (pg_constraint.connamespace = 2200))"}, {" └─ IndexedTableAccess(pg_constraint)"}, {" ├─ index: [pg_constraint.conname,pg_constraint.connamespace]"}, {" └─ filters: [{[test_table1_pkey, test_table1_pkey], [{Namespace:[\"public\"]}, {Namespace:[\"public\"]}]}]"}, @@ -1183,7 +1183,7 @@ func TestPgConstraintIndexes(t *testing.T) { {" ├─ columns: [pg_constraint.conname]"}, {" └─ Sort(pg_constraint.conname ASC)"}, {" └─ Filter"}, - {" ├─ (pg_constraint.conrelid = 3645786842 AND pg_constraint.contypid > 0)"}, + {" ├─ ((pg_constraint.conrelid = 3645786842) AND (pg_constraint.contypid > 0))"}, {" └─ IndexedTableAccess(pg_constraint)"}, {" ├─ index: [pg_constraint.conrelid,pg_constraint.contypid,pg_constraint.conname]"}, {" └─ filters: [{[{Table:[\"public\",\"test_table1\"]}, {Table:[\"public\",\"test_table1\"]}], ({OID:[\"0\"]}, ∞), [NULL, ∞)}]"}, @@ -2199,7 +2199,7 @@ func TestPgNamespaceIndexLookups(t *testing.T) { {" ├─ columns: [pg_namespace.nspname]"}, {" └─ Sort(pg_namespace.nspname ASC)"}, {" └─ Filter"}, - {" ├─ pg_namespace.oid = 2200"}, + {" ├─ (pg_namespace.oid = 2200)"}, {" └─ IndexedTableAccess(pg_namespace)"}, {" ├─ index: [pg_namespace.oid]"}, {" └─ filters: [{[{Namespace:[\"public\"]}, {Namespace:[\"public\"]}]}]"}, @@ -2226,7 +2226,7 @@ func TestPgNamespaceIndexLookups(t *testing.T) { {" ├─ columns: [pg_namespace.oid, pg_namespace.nspname]"}, {" └─ Sort(pg_namespace.oid ASC)"}, {" └─ Filter"}, - {" ├─ (pg_namespace.oid > 11 AND pg_namespace.oid <= 2200)"}, + {" ├─ ((pg_namespace.oid > 11) AND (pg_namespace.oid <= 2200))"}, {" └─ IndexedTableAccess(pg_namespace)"}, {" ├─ index: [pg_namespace.oid]"}, {" └─ filters: [{({Namespace:[\"pg_catalog\"]}, {Namespace:[\"public\"]}]}]"}, @@ -2290,7 +2290,7 @@ func TestPgNamespaceIndexLookups(t *testing.T) { {" ├─ columns: [pg_namespace.oid]"}, {" └─ Sort(pg_namespace.oid ASC)"}, {" └─ Filter"}, - {" ├─ pg_namespace.nspname = 'pg_catalog'"}, + {" ├─ (pg_namespace.nspname = 'pg_catalog')"}, {" └─ IndexedTableAccess(pg_namespace)"}, {" ├─ index: [pg_namespace.nspname]"}, {" └─ filters: [{[pg_catalog, pg_catalog]}]"}, @@ -5779,7 +5779,7 @@ ORDER BY 1;`, {" ├─ columns: [c.oid]"}, {" └─ Sort(c.oid ASC)"}, {" └─ Filter"}, - {" ├─ (c.relname = 't2' AND c.relnamespace = 2200)"}, + {" ├─ ((c.relname = 't2') AND (c.relnamespace = 2200))"}, {" └─ TableAlias(c)"}, {" └─ IndexedTableAccess(pg_class)"}, {" ├─ index: [pg_class.relname,pg_class.relnamespace]"}, @@ -5797,7 +5797,7 @@ ORDER BY 1;`, {" ├─ columns: [c.relname]"}, {" └─ Sort(c.relname ASC)"}, {" └─ Filter"}, - {" ├─ (((c.relname > 't' AND c.relname < 't2') AND c.relnamespace = 2200) AND c.relkind = 'r')"}, + {" ├─ ((((c.relname > 't') AND (c.relname < 't2')) AND (c.relnamespace = 2200)) AND (c.relkind = 'r'))"}, {" └─ TableAlias(c)"}, {" └─ IndexedTableAccess(pg_class)"}, {" ├─ index: [pg_class.relname,pg_class.relnamespace]"}, @@ -5814,7 +5814,7 @@ ORDER BY 1;`, {" ├─ columns: [c.relname]"}, {" └─ Sort(c.relname ASC)"}, {" └─ Filter"}, - {" ├─ c.oid = 1496157034"}, + {" ├─ (c.oid = 1496157034)"}, {" └─ TableAlias(c)"}, {" └─ IndexedTableAccess(pg_class)"}, {" ├─ index: [pg_class.oid]"}, @@ -5831,7 +5831,7 @@ ORDER BY 1;`, {" ├─ columns: [c.relname]"}, {" └─ Sort(c.relname ASC)"}, {" └─ Filter"}, - {" ├─ (c.oid > 1496157033 AND c.oid < 1496157035)"}, + {" ├─ ((c.oid > 1496157033) AND (c.oid < 1496157035))"}, {" └─ TableAlias(c)"}, {" └─ IndexedTableAccess(pg_class)"}, {" ├─ index: [pg_class.oid]"}, @@ -5893,14 +5893,14 @@ ORDER BY 1,2;`, {" ├─ columns: [c.relname, a.attname]"}, {" └─ Sort(c.relname ASC, a.attname ASC)"}, {" └─ InnerJoin"}, - {" ├─ c.oid = a.attrelid"}, + {" ├─ (c.oid = a.attrelid)"}, {" ├─ Filter"}, - {" │ ├─ (a.attnum > 0 AND (NOT(a.attisdropped)))"}, + {" │ ├─ ((a.attnum > 0) AND (NOT(a.attisdropped)))"}, {" │ └─ TableAlias(a)"}, {" │ └─ Table"}, {" │ └─ name: pg_attribute"}, {" └─ Filter"}, - {" ├─ (c.relkind = 'r' AND c.relname = 't2')"}, + {" ├─ ((c.relkind = 'r') AND (c.relname = 't2'))"}, {" └─ TableAlias(c)"}, {" └─ IndexedTableAccess(pg_class)"}, {" ├─ index: [pg_class.relname,pg_class.relnamespace]"}, @@ -5975,7 +5975,7 @@ order by 1,2`, {" └─ Sort(c.relname ASC, n.nspname ASC)"}, {" └─ LookupJoin"}, {" ├─ Filter"}, - {" │ ├─ (c.relname = 't' AND c.relkind = 'r')"}, + {" │ ├─ ((c.relname = 't') AND (c.relkind = 'r'))"}, {" │ └─ TableAlias(c)"}, {" │ └─ IndexedTableAccess(pg_class)"}, {" │ ├─ index: [pg_class.relname,pg_class.relnamespace]"}, @@ -5995,7 +5995,7 @@ order by 1,2`, {" ├─ columns: [c.relname, c.relnamespace]"}, {" └─ Sort(c.relname ASC, c.relnamespace ASC)"}, {" └─ Filter"}, - {" ├─ (c.relname = 't' AND c.relkind = 'r')"}, + {" ├─ ((c.relname = 't') AND (c.relkind = 'r'))"}, {" └─ TableAlias(c)"}, {" └─ IndexedTableAccess(pg_class)"}, {" ├─ index: [pg_class.relname,pg_class.relnamespace]"}, @@ -6090,16 +6090,17 @@ ORDER BY 1;`, {" ├─ columns: [i.indrelid]"}, {" └─ Sort(i.indrelid ASC)"}, {" └─ Filter"}, - {" ├─ i.indexrelid = Subquery"}, + {" ├─ (i.indexrelid = Subquery"}, {" │ ├─ cacheable: true"}, {" │ └─ Project"}, {" │ ├─ columns: [c.oid]"}, {" │ └─ Filter"}, - {" │ ├─ c.relname = 't1_pkey'"}, + {" │ ├─ (c.relname = 't1_pkey')"}, {" │ └─ TableAlias(c)"}, {" │ └─ IndexedTableAccess(pg_class)"}, {" │ ├─ index: [pg_class.relname,pg_class.relnamespace]"}, {" │ └─ filters: [{[t1_pkey, t1_pkey], [NULL, ∞)}]"}, + {" │ )"}, {" └─ TableAlias(i)"}, {" └─ Table"}, {" └─ name: pg_index"}, @@ -6116,7 +6117,7 @@ WHERE i.indrelid = 1496157034 ORDER BY 1`, {" ├─ select: COUNT(1)"}, {" ├─ group: "}, {" └─ Filter"}, - {" ├─ i.indrelid = 1496157034"}, + {" ├─ (i.indrelid = 1496157034)"}, {" └─ TableAlias(i)"}, {" └─ IndexedTableAccess(pg_index)"}, {" ├─ index: [pg_index.indrelid]"}, @@ -6179,7 +6180,7 @@ func TestPgTypeIndexes(t *testing.T) { {" ├─ columns: [pg_type.typname]"}, {" └─ Sort(pg_type.typname ASC)"}, {" └─ Filter"}, - {" ├─ pg_type.oid = 23"}, + {" ├─ (pg_type.oid = 23)"}, {" └─ IndexedTableAccess(pg_type)"}, {" ├─ index: [pg_type.oid]"}, {" └─ filters: [{[{Type:[\"pg_catalog\",\"int4\"]}, {Type:[\"pg_catalog\",\"int4\"]}]}]"}, @@ -6192,7 +6193,7 @@ func TestPgTypeIndexes(t *testing.T) { {" ├─ columns: [pg_type.typname]"}, {" └─ Sort(pg_type.typname ASC)"}, {" └─ Filter"}, - {" ├─ (pg_type.oid > 22 AND pg_type.oid < 25)"}, + {" ├─ ((pg_type.oid > 22) AND (pg_type.oid < 25))"}, {" └─ IndexedTableAccess(pg_type)"}, {" ├─ index: [pg_type.oid]"}, {" └─ filters: [{({Type:[\"pg_catalog\",\"int2vector\"]}, {Type:[\"pg_catalog\",\"text\"]})}]"}, @@ -6243,7 +6244,7 @@ func TestPgTypeIndexes(t *testing.T) { {" ├─ columns: [pg_type.oid]"}, {" └─ Sort(pg_type.oid ASC)"}, {" └─ Filter"}, - {" ├─ (pg_type.typname = 'int4' AND pg_type.typnamespace = 11)"}, + {" ├─ ((pg_type.typname = 'int4') AND (pg_type.typnamespace = 11))"}, {" └─ IndexedTableAccess(pg_type)"}, {" ├─ index: [pg_type.typname,pg_type.typnamespace]"}, {" └─ filters: [{[int4, int4], [{Namespace:[\"pg_catalog\"]}, {Namespace:[\"pg_catalog\"]}]}]"}, @@ -6256,7 +6257,7 @@ func TestPgTypeIndexes(t *testing.T) { {" ├─ columns: [pg_type.typname]"}, {" └─ Sort(pg_type.typname ASC)"}, {" └─ Filter"}, - {" ├─ ((pg_type.typname > 'int2' AND pg_type.typname < 'int8') AND pg_type.typnamespace = 11)"}, + {" ├─ (((pg_type.typname > 'int2') AND (pg_type.typname < 'int8')) AND (pg_type.typnamespace = 11))"}, {" └─ IndexedTableAccess(pg_type)"}, {" ├─ index: [pg_type.typname,pg_type.typnamespace]"}, {" └─ filters: [{(int2, int8), [{Namespace:[\"pg_catalog\"]}, {Namespace:[\"pg_catalog\"]}]}]"}, @@ -6287,7 +6288,7 @@ func TestPgTypeIndexes(t *testing.T) { {" └─ Sort(t.typname ASC)"}, {" └─ LookupJoin"}, {" ├─ Filter"}, - {" │ ├─ t.typname = 'int4'"}, + {" │ ├─ (t.typname = 'int4')"}, {" │ └─ TableAlias(t)"}, {" │ └─ IndexedTableAccess(pg_type)"}, {" │ ├─ index: [pg_type.typname,pg_type.typnamespace]"}, diff --git a/testing/go/stats_usage_test.go b/testing/go/stats_usage_test.go index e8a8481325..fd7efaf5a0 100644 --- a/testing/go/stats_usage_test.go +++ b/testing/go/stats_usage_test.go @@ -273,7 +273,7 @@ var StatsUsageTests = []ScriptTest{ Query: "EXPLAIN SELECT * FROM big JOIN small ON big.val = small.val;", Expected: []sql.Row{ {"HashJoin"}, - {" ├─ big.val = small.val"}, + {" ├─ (big.val = small.val)"}, {" ├─ Table"}, {" │ ├─ name: big"}, {" │ └─ columns: [pk val]"}, @@ -292,7 +292,7 @@ var StatsUsageTests = []ScriptTest{ {"Project"}, {" ├─ columns: [small.pk, small.val, big.pk, big.val]"}, {" └─ HashJoin"}, - {" ├─ big.val = small.val"}, + {" ├─ (big.val = small.val)"}, {" ├─ Table"}, {" │ ├─ name: big"}, {" │ └─ columns: [pk val]"}, @@ -324,7 +324,7 @@ var StatsUsageTests = []ScriptTest{ Query: "EXPLAIN SELECT * FROM t WHERE lowcard = 3 AND highcard = 42;", Expected: []sql.Row{ {"Filter"}, - {" ├─ t.lowcard = 3"}, + {" ├─ (t.lowcard = 3)"}, {" └─ IndexedTableAccess(t)"}, {" ├─ index: [t.highcard]"}, {" ├─ filters: [{[42, 42]}]"}, @@ -337,7 +337,7 @@ var StatsUsageTests = []ScriptTest{ Query: "EXPLAIN SELECT * FROM t WHERE lowcard = 3 AND highcard > 0;", Expected: []sql.Row{ {"Filter"}, - {" ├─ t.highcard > 0"}, + {" ├─ (t.highcard > 0)"}, {" └─ IndexedTableAccess(t)"}, {" ├─ index: [t.lowcard]"}, {" ├─ filters: [{[3, 3]}]"}, diff --git a/testing/go/subqueries_test.go b/testing/go/subqueries_test.go index 427555bc55..2b72275fc4 100755 --- a/testing/go/subqueries_test.go +++ b/testing/go/subqueries_test.go @@ -283,7 +283,7 @@ func TestExistSubquery(t *testing.T) { Query: `EXPLAIN SELECT * FROM a WHERE EXISTS (SELECT 1 FROM b WHERE a.x = b.x);`, Expected: []sql.Row{ {"SemiJoin"}, - {" ├─ a.x = b.x"}, + {" ├─ (a.x = b.x)"}, {" ├─ Table"}, {" │ └─ name: a"}, {" └─ Table"}, @@ -306,7 +306,7 @@ func TestExistSubquery(t *testing.T) { {" └─ Filter"}, {" ├─ 1 IS NULL"}, {" └─ LeftOuterJoin"}, - {" ├─ a.x = b.x"}, + {" ├─ (a.x = b.x)"}, {" ├─ Table"}, {" │ └─ name: a"}, {" └─ Project"},