diff --git a/go.mod b/go.mod index 99208147de..1c19dac788 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( github.com/PuerkitoBio/goquery v1.8.1 github.com/cockroachdb/apd/v3 v3.2.3 github.com/cockroachdb/errors v1.7.5 - github.com/dolthub/dolt/go v0.40.5-0.20260828173935-dc4e79209ce0 + github.com/dolthub/dolt/go v0.40.5-0.20260901102237-645f6accd917 github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 - github.com/dolthub/go-mysql-server v0.20.1-0.20260831214236-aa1f8e45655b + github.com/dolthub/go-mysql-server v0.20.1-0.20260901192735-5d6be3976cd5 github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 github.com/dolthub/vitess v0.0.0-20260828193927-f9eb707fd659 diff --git a/go.sum b/go.sum index 1b048697d6..e2c68403b3 100644 --- a/go.sum +++ b/go.sum @@ -246,8 +246,8 @@ github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12 h1:I github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12/go.mod h1:rN7X8BHwkjPcfMQQ2QTAq/xM3leUSGLfb+1Js7Y6TVo= github.com/dolthub/dolt-mcp v0.3.4 h1:AyG5cw+fNWXDHXujtQnqUPZrpWtPg6FN6yYtjv1pP44= github.com/dolthub/dolt-mcp v0.3.4/go.mod h1:bCZ7KHvDYs+M0e+ySgmGiNvLhcwsN7bbf5YCyillLrk= -github.com/dolthub/dolt/go v0.40.5-0.20260828173935-dc4e79209ce0 h1:VrCgqBaqm3WT7olZRdYGScPGsZdzd4S67M/hD/R00f8= -github.com/dolthub/dolt/go v0.40.5-0.20260828173935-dc4e79209ce0/go.mod h1:vGmWnRXHjHe+BYLeaC2WCte5lpzEXdcKFt4mFiKD2yc= +github.com/dolthub/dolt/go v0.40.5-0.20260901102237-645f6accd917 h1:lx7XhihYV9y0H5N6US13VlN5JGjT7FB9eksXXA7sMCU= +github.com/dolthub/dolt/go v0.40.5-0.20260901102237-645f6accd917/go.mod h1:D1MXT5V70cZfYR8eKpijs/1vsOFgSGdmr9hk83gi73Y= github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 h1:0mg9QEFdkkBwJMxvz1tCjHYmfG2iIC6aShj1InDq9/M= github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4/go.mod h1:SSLraQS/jGLYFgff3vuZ+JbVUct6vyEeMzjLBqWqoyM= github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= @@ -256,8 +256,8 @@ github.com/dolthub/fslock v0.0.5 h1:QoXhBhgY1oumHE26qyE7tgmXUT8qjJwxsIzo54O/B/k= github.com/dolthub/fslock v0.0.5/go.mod h1:sdofYYqE0D79zNZyB4/kmlnsQOVap1C2yByjGKSirEM= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83 h1:FEMjCGEroDnY/BXyAffVZxUpXhP2GpoUJyyq5KaLn8c= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= -github.com/dolthub/go-mysql-server v0.20.1-0.20260831214236-aa1f8e45655b h1:O9lohpiOrRXsdogCmnxLT2P7ox4u+Ilat6pl6b8f2+w= -github.com/dolthub/go-mysql-server v0.20.1-0.20260831214236-aa1f8e45655b/go.mod h1:CtDdfkAma4klv+UQbEmV4AkVvkSyXf/V3iRpIDu6c2I= +github.com/dolthub/go-mysql-server v0.20.1-0.20260901192735-5d6be3976cd5 h1:OjIEbos9AZW660TkKANOzDjMnOZgFchyJkhcXXulCcE= +github.com/dolthub/go-mysql-server v0.20.1-0.20260901192735-5d6be3976cd5/go.mod h1:CtDdfkAma4klv+UQbEmV4AkVvkSyXf/V3iRpIDu6c2I= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20260414231531-5f031e3e9037 h1:oIW9HwuWrhxv+4HZxA+QQSKHLqWFyXZ2FmNjUYwkdiM= diff --git a/server/ast/set_var.go b/server/ast/set_var.go index 9ba477f823..4d8e5b6b87 100644 --- a/server/ast/set_var.go +++ b/server/ast/set_var.go @@ -20,6 +20,7 @@ import ( "github.com/cockroachdb/errors" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/planbuilder" vitess "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/dolthub/doltgresql/postgres/parser/sem/tree" @@ -42,9 +43,9 @@ func nodeSetVar(ctx *Context, node *tree.SetVar) (vitess.Statement, error) { if node.Namespace == "" && !config.IsValidPostgresConfigParameter(node.Name) && !config.IsValidDoltConfigParameter(node.Name) { return nil, errors.Errorf(`ERROR: unrecognized configuration parameter "%s"`, node.Name) } - if node.IsLocal { - // TODO: takes effect for only the current transaction rather than the current session. - return nil, errors.Errorf("SET LOCAL is not yet supported") + if node.IsLocal && node.Namespace != "" { + // TODO: support transaction-local values for custom (namespaced) parameters, which are session user vars + return nil, errors.Errorf("SET LOCAL is not yet supported for customized options") } var expr vitess.Expr var err error @@ -71,7 +72,11 @@ func nodeSetVar(ctx *Context, node *tree.SetVar) (vitess.Statement, error) { // Dolt's cluster replication variables) are routed to their declared scope directly, symmetric with // current_setting() reading them from the global scope. scope := vitess.SetScope_Session - if svScope, ok := config.GlobalOnlySystemVariableScope(node.Name); ok { + if node.IsLocal { + // SET LOCAL only applies for the duration of the current transaction; the connection handler restores + // the session values when the transaction ends. Global-only variables are rejected by the engine. + scope = planbuilder.SetScope_TransactionLocal + } else if svScope, ok := config.GlobalOnlySystemVariableScope(node.Name); ok { switch svScope { case sql.SystemVariableScope_Persist: scope = vitess.SetScope_Persist diff --git a/server/config/parameters.go b/server/config/parameters.go index 0d4c029b80..4c413570f0 100644 --- a/server/config/parameters.go +++ b/server/config/parameters.go @@ -97,6 +97,12 @@ func (p *Parameter) GetSessionScope() sql.SystemVariableScope { return GetPgsqlScope(PsqlScopeSession) } +// GetLocalScope implements sql.SystemVariable. Any parameter that can be set at session scope can also be set with +// transaction-local scope (SET LOCAL). +func (p *Parameter) GetLocalScope() sql.SystemVariableScope { + return GetPgsqlScope(PsqlScopeLocal) +} + // SetDefault implements sql.SystemVariable. func (p *Parameter) SetDefault(a any) { if validatedVal, ok := p.ValidateFunc(p.Default, a); ok { @@ -223,8 +229,8 @@ func (p *PgsqlScope) SetValue(ctx *sql.Context, name string, val any) error { err := ctx.SetSessionVariable(ctx, name, val) return err case PsqlScopeLocal: - // TODO: support LOCAL scope - return cerrors.Errorf("unsupported scope `%v` on configuration parameter `%s`", p.Type, name) + _ = core.SetDateStyleOutputFormat(ctx, "") + return ctx.Session.SetTransactionLocalVariable(ctx, name, val) default: return cerrors.Errorf("unable to set `%s` due to unknown scope `%v`", name, p.Type) } @@ -240,8 +246,12 @@ func (p *PgsqlScope) GetValue(ctx *sql.Context, name string, _ sql.CollationID) } return val, nil case PsqlScopeLocal: - // TODO: support LOCAL scope - return nil, cerrors.Errorf("unsupported scope `%v` on configuration parameter `%s`", p.Type, name) + // The session value already reflects any transaction-local override + val, err := ctx.GetSessionVariable(ctx, name) + if err != nil { + return nil, err + } + return val, nil default: return nil, cerrors.Errorf("unknown scope `%v` on configuration parameter `%s`", p.Type, name) } diff --git a/server/connection_handler.go b/server/connection_handler.go index 3f0e66454a..f08bfef9e6 100644 --- a/server/connection_handler.go +++ b/server/connection_handler.go @@ -44,6 +44,7 @@ import ( "github.com/mitchellh/go-ps" "github.com/sirupsen/logrus" + "github.com/dolthub/doltgresql/core" "github.com/dolthub/doltgresql/core/dataloader" "github.com/dolthub/doltgresql/postgres/parser/parser" psql "github.com/dolthub/doltgresql/postgres/parser/parser/sql" @@ -604,6 +605,7 @@ func (h *ConnectionHandler) handleQueryOutsideEngine(query ConvertedQuery) (hand // A COMMIT issued inside a failed transaction block ends the block by rolling it back, and reports // ROLLBACK to the client to indicate that the transaction's effects were discarded. h.transactionState = idleTransactionState + h.clearTransactionLocalVars() if err := h.runEngineTransactionControl("ROLLBACK"); err != nil { return true, true, err } @@ -612,9 +614,11 @@ func (h *ConnectionHandler) handleQueryOutsideEngine(query ConvertedQuery) (hand // A COMMIT closes the current transaction block, whether explicit or implicit. Any statements that // follow it in the same Query message (or extended-query batch) run in a new implicit transaction block. h.transactionState = idleTransactionState + h.clearTransactionLocalVars() case *sqlparser.Rollback: // Like COMMIT, a ROLLBACK closes the current transaction block, whether explicit, implicit, or failed. h.transactionState = idleTransactionState + h.clearTransactionLocalVars() case *sqlparser.Savepoint: if !h.transactionState.inExplicitTransactionBlock() { return true, true, noActiveTransactionError("SAVEPOINT") @@ -1326,6 +1330,7 @@ func (h *ConnectionHandler) commitImplicitTransaction() error { return nil } h.transactionState = idleTransactionState + h.clearTransactionLocalVars() if h.restoredAutoCommitWithoutTransaction() { return nil } @@ -1344,6 +1349,7 @@ func (h *ConnectionHandler) rollbackImplicitTransaction() { return } h.transactionState = idleTransactionState + h.clearTransactionLocalVars() if h.restoredAutoCommitWithoutTransaction() { return } @@ -1352,6 +1358,23 @@ func (h *ConnectionHandler) rollbackImplicitTransaction() { } } +// clearTransactionLocalVars removes any system variable values that were set with transaction-local scope +// (SET LOCAL, or set_config with is_local), restoring the variables' session values. Called whenever the current +// transaction block ends, whether by COMMIT or ROLLBACK: Postgres reverts SET LOCAL values in both cases. +func (h *ConnectionHandler) clearTransactionLocalVars() { + ctx, err := h.doltgresHandler.NewContext(context.Background(), h.mysqlConn, "") + if err != nil { + logrus.Warnf("error creating context to clear transaction-local variables: %s", err) + return + } + if err = ctx.Session.ClearTransactionLocalVariables(ctx); err != nil { + logrus.Warnf("error clearing transaction-local variables: %s", err) + return + } + // Reset any cached variables in ContextValues, in case a cached parameter (e.g. datestyle) was overridden + _ = core.SetDateStyleOutputFormat(ctx, "") +} + // restoredAutoCommitWithoutTransaction returns whether the session no longer has an engine transaction in // progress, restoring the session's autocommit behavior if so. Some statements end the engine transaction // themselves as a side effect of executing (e.g. dolt_assume_cluster_role, which also poisons the session diff --git a/server/functions/set_config.go b/server/functions/set_config.go index 774c048952..be1234d82c 100644 --- a/server/functions/set_config.go +++ b/server/functions/set_config.go @@ -44,11 +44,6 @@ var set_config_text_text_boolean = framework.Function3{ newValue = "" } - if isLocal == true { - // TODO: If isLocal is true, then the config setting should only persist for the current transaction - return nil, errors.Errorf("setting configuration values for the current transaction is not supported yet") - } - settingNameStr, err := framework.UnwrapString(ctx, settingName) if err != nil { return nil, err @@ -61,7 +56,13 @@ var set_config_text_text_boolean = framework.Function3{ // set_config can set system configuration or user configuration. System configuration settings are in top // level settings, while user configuration settings are namespaced. isUserConfig := strings.Contains(settingNameStr, ".") - if isUserConfig { + if isLocal == true { + // A transaction-local value overrides the session value until the transaction ends, when the + // connection handler clears it + if err := ctx.Session.SetTransactionLocalVariable(ctx, settingName.(string), newValue.(string)); err != nil { + return nil, err + } + } else if isUserConfig { if err := ctx.SetUserVariable(ctx, settingNameStr, newValueStr, pgtypes.Text); err != nil { return nil, err } diff --git a/testing/generation/command_docs/output/set_test.go b/testing/generation/command_docs/output/set_test.go index 015c9c4bb5..97b2b4a96b 100644 --- a/testing/generation/command_docs/output/set_test.go +++ b/testing/generation/command_docs/output/set_test.go @@ -38,16 +38,16 @@ func TestSet(t *testing.T) { Parses("SET LOCAL configuration_parameter = DEFAULT"), Converts("SET TIME ZONE 1"), Converts("SET SESSION TIME ZONE 1"), - Parses("SET LOCAL TIME ZONE 1"), + Converts("SET LOCAL TIME ZONE 1"), Converts("SET TIME ZONE ' 1 '"), Converts("SET SESSION TIME ZONE ' 1 '"), - Parses("SET LOCAL TIME ZONE ' 1 '"), + Converts("SET LOCAL TIME ZONE ' 1 '"), Converts("SET TIME ZONE LOCAL"), Converts("SET SESSION TIME ZONE LOCAL"), - Parses("SET LOCAL TIME ZONE LOCAL"), + Converts("SET LOCAL TIME ZONE LOCAL"), Converts("SET TIME ZONE DEFAULT"), Converts("SET SESSION TIME ZONE DEFAULT"), - Parses("SET LOCAL TIME ZONE DEFAULT"), + Converts("SET LOCAL TIME ZONE DEFAULT"), } RunTests(t, tests) } diff --git a/testing/go/set_local_test.go b/testing/go/set_local_test.go new file mode 100644 index 0000000000..815561eee6 --- /dev/null +++ b/testing/go/set_local_test.go @@ -0,0 +1,234 @@ +// 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 _go + +import ( + "testing" + + "github.com/dolthub/go-mysql-server/sql" +) + +func TestSetLocal(t *testing.T) { + RunScripts(t, []ScriptTest{ + { + Name: "SET LOCAL reverts on COMMIT", + Assertions: []ScriptTestAssertion{ + { + Query: "BEGIN", + }, + { + Query: "SET LOCAL enable_hashjoin = off", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{0}}, + }, + { + Query: "COMMIT", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{1}}, + }, + }, + }, + { + Name: "SET LOCAL reverts on ROLLBACK", + Assertions: []ScriptTestAssertion{ + { + Query: "BEGIN", + }, + { + Query: "SET LOCAL enable_hashjoin = off", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{0}}, + }, + { + Query: "ROLLBACK", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{1}}, + }, + }, + }, + { + Name: "SET LOCAL reverts to the session value, not the default", + Assertions: []ScriptTestAssertion{ + { + Query: "SET enable_hashjoin = off", + }, + { + Query: "BEGIN", + }, + { + Query: "SET LOCAL enable_hashjoin = on", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{1}}, + }, + { + Query: "COMMIT", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{0}}, + }, + { + Query: "SET enable_hashjoin = on", + }, + }, + }, + { + Name: "SET LOCAL reverts when a failed transaction is rolled back", + SetUpScript: []string{ + `CREATE TABLE test (a INT PRIMARY KEY)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "BEGIN", + }, + { + Query: "SET LOCAL enable_hashjoin = off", + }, + { + Query: "SELECT no_such_column FROM test", + ExpectedErr: "could not be found", + }, + { + Query: "SHOW enable_hashjoin", + ExpectedErr: "current transaction is aborted", + }, + { + Query: "ROLLBACK", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{1}}, + }, + }, + }, + { + Name: "SET LOCAL outside a transaction block has no lasting effect", + Assertions: []ScriptTestAssertion{ + { + Query: "SET LOCAL enable_hashjoin = off", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{1}}, + }, + }, + }, + { + Name: "SET LOCAL with savepoints does not abort the transaction", + SetUpScript: []string{ + `CREATE TABLE test (a INT PRIMARY KEY)`, + `INSERT INTO test VALUES (1)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "BEGIN", + }, + { + Query: "SAVEPOINT settings", + }, + { + Query: "SET LOCAL enable_hashjoin = off", + }, + { + Query: "SET LOCAL enable_mergejoin = on", + }, + { + Query: "SELECT * FROM test", + Expected: []sql.Row{{1}}, + }, + { + Query: "ROLLBACK TO settings", + }, + { + Query: "SELECT * FROM test", + Expected: []sql.Row{{1}}, + }, + { + Query: "COMMIT", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{1}}, + }, + }, + }, + { + Name: "SET after SET LOCAL persists after COMMIT", + Assertions: []ScriptTestAssertion{ + { + Query: "BEGIN", + }, + { + Query: "SET LOCAL enable_hashjoin = off", + }, + { + Query: "SET enable_hashjoin = off", + }, + { + Query: "COMMIT", + }, + { + Query: "SHOW enable_hashjoin", + Expected: []sql.Row{{0}}, + }, + { + Query: "SET enable_hashjoin = on", + }, + }, + }, + { + Name: "SET LOCAL on an unknown parameter errors", + Assertions: []ScriptTestAssertion{ + { + Query: "SET LOCAL no_such_parameter = on", + ExpectedErr: "unrecognized configuration parameter", + }, + }, + }, + { + Name: "set_config with is_local reverts on COMMIT", + Assertions: []ScriptTestAssertion{ + { + Query: "BEGIN", + }, + { + Query: "SELECT set_config('enable_seqscan', 'off', true)", + Expected: []sql.Row{{"off"}}, + }, + { + Query: "SHOW enable_seqscan", + Expected: []sql.Row{{0}}, + }, + { + Query: "COMMIT", + }, + { + Query: "SHOW enable_seqscan", + Expected: []sql.Row{{1}}, + }, + }, + }, + }) +}