From e8b1e3feb3c78bed1e3321fdf8475f22b8947b93 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 6 Feb 2022 14:41:41 -0500 Subject: [PATCH] sql: add SHOW TRANSFER STATE observer statement Informs #76000. This commit adds the SHOW TRANSFER STATE observer statement, as described in the sqlproxy connection migration RFC. This observer statement will be used whenever a connection is about to be migrated to retrieve the relevant values needed for the transfer process. A unique aspect to this statement is that serialization or token generation errors will be returned as a SQL value instead of an ErrorResponse. This will allow the sqlproxy to react accordingly, and to reduce ambiguity issues during transferring. This observer statement will only work on tenants (due to the need of token generation). Since this is meant to be used internally only, there is no release note. Release note: None --- docs/generated/sql/bnf/BUILD.bazel | 1 + docs/generated/sql/bnf/show_transfer_stmt.bnf | 3 + docs/generated/sql/bnf/show_var.bnf | 1 + docs/generated/sql/bnf/stmt_block.bnf | 7 + pkg/ccl/testccl/sqlccl/BUILD.bazel | 2 + .../sqlccl/show_transfer_state_test.go | 259 ++++++++++++++++++ pkg/sql/BUILD.bazel | 1 + pkg/sql/conn_executor_exec.go | 94 +++++++ pkg/sql/conn_executor_test.go | 76 +++++ pkg/sql/delegate/delegate.go | 12 +- pkg/sql/faketreeeval/evalctx.go | 10 + .../logictest/testdata/logic_test/show_source | 6 + pkg/sql/parser/help_test.go | 5 + pkg/sql/parser/sql.y | 26 +- pkg/sql/parser/testdata/show | 22 ++ pkg/sql/sem/builtins/builtins.go | 76 +---- pkg/sql/sem/tree/eval.go | 8 + pkg/sql/sem/tree/show.go | 19 ++ pkg/sql/sem/tree/stmt.go | 12 + pkg/sql/session_revival_token.go | 30 +- pkg/sql/session_state.go | 119 ++++++++ 21 files changed, 705 insertions(+), 84 deletions(-) create mode 100644 docs/generated/sql/bnf/show_transfer_stmt.bnf create mode 100644 pkg/ccl/testccl/sqlccl/show_transfer_state_test.go create mode 100644 pkg/sql/session_state.go diff --git a/docs/generated/sql/bnf/BUILD.bazel b/docs/generated/sql/bnf/BUILD.bazel index 9be5f5e46728..9b786300c533 100644 --- a/docs/generated/sql/bnf/BUILD.bazel +++ b/docs/generated/sql/bnf/BUILD.bazel @@ -205,6 +205,7 @@ FILES = [ "show_tables", "show_trace", "show_transactions_stmt", + "show_transfer_stmt", "show_types_stmt", "show_users_stmt", "show_var", diff --git a/docs/generated/sql/bnf/show_transfer_stmt.bnf b/docs/generated/sql/bnf/show_transfer_stmt.bnf new file mode 100644 index 000000000000..0b4d449df32e --- /dev/null +++ b/docs/generated/sql/bnf/show_transfer_stmt.bnf @@ -0,0 +1,3 @@ +show_transfer_stmt ::= + 'SHOW' 'TRANSFER' 'STATE' 'WITH' non_reserved_word_or_sconst + | 'SHOW' 'TRANSFER' 'STATE' diff --git a/docs/generated/sql/bnf/show_var.bnf b/docs/generated/sql/bnf/show_var.bnf index 3266ba0cc89a..ffb29fc3e812 100644 --- a/docs/generated/sql/bnf/show_var.bnf +++ b/docs/generated/sql/bnf/show_var.bnf @@ -29,6 +29,7 @@ show_stmt ::= | show_tables_stmt | show_trace_stmt | show_transactions_stmt + | show_transfer_stmt | show_users_stmt | show_zone_stmt | show_full_scans_stmt diff --git a/docs/generated/sql/bnf/stmt_block.bnf b/docs/generated/sql/bnf/stmt_block.bnf index e3b87be598bc..58f3e818595c 100644 --- a/docs/generated/sql/bnf/stmt_block.bnf +++ b/docs/generated/sql/bnf/stmt_block.bnf @@ -243,6 +243,7 @@ show_stmt ::= | show_tables_stmt | show_trace_stmt | show_transactions_stmt + | show_transfer_stmt | show_users_stmt | show_zone_stmt | show_full_scans_stmt @@ -778,6 +779,10 @@ show_transactions_stmt ::= 'SHOW' opt_cluster 'TRANSACTIONS' | 'SHOW' 'ALL' opt_cluster 'TRANSACTIONS' +show_transfer_stmt ::= + 'SHOW' 'TRANSFER' 'STATE' 'WITH' non_reserved_word_or_sconst + | 'SHOW' 'TRANSFER' 'STATE' + show_users_stmt ::= 'SHOW' 'USERS' @@ -1156,6 +1161,7 @@ unreserved_keyword ::= | 'SQL' | 'SQLLOGIN' | 'START' + | 'STATE' | 'STATEMENTS' | 'STATISTICS' | 'STDIN' @@ -1182,6 +1188,7 @@ unreserved_keyword ::= | 'TRACE' | 'TRANSACTION' | 'TRANSACTIONS' + | 'TRANSFER' | 'TRIGGER' | 'TRUNCATE' | 'TRUSTED' diff --git a/pkg/ccl/testccl/sqlccl/BUILD.bazel b/pkg/ccl/testccl/sqlccl/BUILD.bazel index 9056ce90d86b..a75377363cb4 100644 --- a/pkg/ccl/testccl/sqlccl/BUILD.bazel +++ b/pkg/ccl/testccl/sqlccl/BUILD.bazel @@ -6,6 +6,7 @@ go_test( "main_test.go", "run_control_test.go", "session_revival_test.go", + "show_transfer_state_test.go", "temp_table_clean_test.go", ], deps = [ @@ -31,6 +32,7 @@ go_test( "//pkg/util/stop", "//pkg/util/syncutil", "//pkg/util/timeutil", + "@com_github_cockroachdb_cockroach_go_v2//crdb", "@com_github_cockroachdb_errors//:errors", "@com_github_gogo_protobuf//types", "@com_github_jackc_pgx_v4//:pgx", diff --git a/pkg/ccl/testccl/sqlccl/show_transfer_state_test.go b/pkg/ccl/testccl/sqlccl/show_transfer_state_test.go new file mode 100644 index 000000000000..34e4b357e89d --- /dev/null +++ b/pkg/ccl/testccl/sqlccl/show_transfer_state_test.go @@ -0,0 +1,259 @@ +// Copyright 2022 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package sqlccl + +import ( + "context" + gosql "database/sql" + "net/url" + "testing" + + "github.com/cockroachdb/cockroach-go/v2/crdb" + "github.com/cockroachdb/cockroach/pkg/security" + "github.com/cockroachdb/cockroach/pkg/sql/tests" + "github.com/cockroachdb/cockroach/pkg/testutils/serverutils" + "github.com/cockroachdb/cockroach/pkg/testutils/sqlutils" + "github.com/cockroachdb/cockroach/pkg/util/leaktest" + "github.com/stretchr/testify/require" +) + +func TestShowTransferState(t *testing.T) { + defer leaktest.AfterTest(t)() + ctx := context.Background() + + params, _ := tests.CreateTestServerParams() + s, _, _ := serverutils.StartServer(t, params) + defer s.Stopper().Stop(ctx) + tenant, mainDB := serverutils.StartTenant(t, s, tests.CreateTestTenantParams(serverutils.TestTenantID())) + defer tenant.Stopper().Stop(ctx) + defer mainDB.Close() + + _, err := mainDB.Exec("CREATE USER testuser WITH PASSWORD 'hunter2'") + require.NoError(t, err) + + t.Run("without_transfer_key", func(t *testing.T) { + pgURL, cleanup := sqlutils.PGUrl( + t, + tenant.SQLAddr(), + "TestShowTransferState-without_transfer_key", + url.UserPassword(security.TestUser, "hunter2"), + ) + defer cleanup() + + conn, err := gosql.Open("postgres", pgURL.String()) + require.NoError(t, err) + defer conn.Close() + + rows, err := conn.Query("SHOW TRANSFER STATE") + require.NoError(t, err, "show transfer state failed") + defer rows.Close() + + resultColumns, err := rows.Columns() + require.NoError(t, err) + + const expectedNumColumns = 3 + if len(resultColumns) != expectedNumColumns { + t.Fatalf( + "unexpected number of columns in result; expected %d, found %d", + expectedNumColumns, + len(resultColumns), + ) + } + + var errVal, sessionState, sessionRevivalToken gosql.NullString + + rows.Next() + err = rows.Scan(&errVal, &sessionState, &sessionRevivalToken) + require.NoError(t, err, "unexpected error while reading transfer state") + + require.False(t, errVal.Valid) + require.True(t, sessionState.Valid) + require.True(t, sessionRevivalToken.Valid) + }) + + var state, token string + t.Run("with_transfer_key", func(t *testing.T) { + pgURL, cleanup := sqlutils.PGUrl( + t, + tenant.SQLAddr(), + "TestShowTransferState-with_transfer_key", + url.UserPassword(security.TestUser, "hunter2"), + ) + defer cleanup() + + q := pgURL.Query() + q.Add("application_name", "carl") + pgURL.RawQuery = q.Encode() + conn, err := gosql.Open("postgres", pgURL.String()) + require.NoError(t, err) + defer conn.Close() + + rows, err := conn.Query("SHOW TRANSFER STATE WITH 'foobar'") + require.NoError(t, err, "show transfer state failed") + defer rows.Close() + + resultColumns, err := rows.Columns() + require.NoError(t, err) + + const expectedNumColumns = 4 + if len(resultColumns) != expectedNumColumns { + t.Fatalf( + "unexpected number of columns in result; expected %d, found %d", + expectedNumColumns, + len(resultColumns), + ) + } + + var key string + var errVal, sessionState, sessionRevivalToken gosql.NullString + + rows.Next() + err = rows.Scan(&key, &errVal, &sessionState, &sessionRevivalToken) + require.NoError(t, err, "unexpected error while reading transfer state") + + require.Equal(t, "foobar", key) + require.False(t, errVal.Valid) + require.True(t, sessionState.Valid) + require.True(t, sessionRevivalToken.Valid) + state = sessionState.String + token = sessionRevivalToken.String + }) + + t.Run("successful_transfer", func(t *testing.T) { + pgURL, cleanup := sqlutils.PGUrl( + t, + tenant.SQLAddr(), + "TestShowTransferState-successful_transfer", + url.User(security.TestUser), // Do not use a password here. + ) + defer cleanup() + + q := pgURL.Query() + q.Add("application_name", "someotherapp") + q.Add("crdb:session_revival_token_base64", token) + pgURL.RawQuery = q.Encode() + conn, err := gosql.Open("postgres", pgURL.String()) + require.NoError(t, err) + defer conn.Close() + + var appName string + err = conn.QueryRow("SHOW application_name").Scan(&appName) + require.NoError(t, err) + require.Equal(t, "someotherapp", appName) + + var b bool + err = conn.QueryRow( + "SELECT crdb_internal.deserialize_session(decode($1, 'base64'))", + state, + ).Scan(&b) + require.NoError(t, err) + require.True(t, b) + + err = conn.QueryRow("SHOW application_name").Scan(&appName) + require.NoError(t, err) + require.Equal(t, "carl", appName) + }) + + // Errors should be displayed as a SQL value. + t.Run("errors", func(t *testing.T) { + t.Run("root_user", func(t *testing.T) { + var key string + var errVal, sessionState, sessionRevivalToken gosql.NullString + err := mainDB.QueryRow("SHOW TRANSFER STATE WITH 'bar'").Scan(&key, &errVal, &sessionState, &sessionRevivalToken) + require.NoError(t, err) + + require.True(t, errVal.Valid) + require.Equal(t, "cannot create token for root user", errVal.String) + require.False(t, sessionState.Valid) + require.False(t, sessionRevivalToken.Valid) + }) + + t.Run("transaction", func(t *testing.T) { + pgURL, cleanup := sqlutils.PGUrl( + t, + tenant.SQLAddr(), + "TestShowTransferState-errors-transaction", + url.UserPassword(security.TestUser, "hunter2"), + ) + defer cleanup() + + conn, err := gosql.Open("postgres", pgURL.String()) + require.NoError(t, err) + defer conn.Close() + + var errVal, sessionState, sessionRevivalToken gosql.NullString + err = crdb.ExecuteTx(ctx, conn, nil /* txopts */, func(tx *gosql.Tx) error { + return tx.QueryRow("SHOW TRANSFER STATE").Scan(&errVal, &sessionState, &sessionRevivalToken) + }) + require.NoError(t, err) + + require.True(t, errVal.Valid) + require.Equal(t, "cannot serialize a session which is inside a transaction", errVal.String) + require.False(t, sessionState.Valid) + require.False(t, sessionRevivalToken.Valid) + }) + + t.Run("prepared_statements", func(t *testing.T) { + pgURL, cleanup := sqlutils.PGUrl( + t, + tenant.SQLAddr(), + "TestShowTransferState-errors-prepared_statements", + url.UserPassword(security.TestUser, "hunter2"), + ) + defer cleanup() + + conn, err := gosql.Open("postgres", pgURL.String()) + require.NoError(t, err) + defer conn.Close() + + // Use a dummy prepared statement. + stmt, err := conn.Prepare("SELECT 1 WHERE 1 = 1") + require.NoError(t, err) + defer stmt.Close() + + var errVal, sessionState, sessionRevivalToken gosql.NullString + err = conn.QueryRow("SHOW TRANSFER STATE").Scan(&errVal, &sessionState, &sessionRevivalToken) + require.NoError(t, err) + + require.True(t, errVal.Valid) + require.Equal(t, "cannot serialize a session which has portals or prepared statements", errVal.String) + require.False(t, sessionState.Valid) + require.False(t, sessionRevivalToken.Valid) + }) + + t.Run("temp_tables", func(t *testing.T) { + pgURL, cleanup := sqlutils.PGUrl( + t, + tenant.SQLAddr(), + "TestShowTransferState-errors-temp_tables", + url.UserPassword(security.TestUser, "hunter2"), + ) + defer cleanup() + + q := pgURL.Query() + q.Add("experimental_enable_temp_tables", "true") + pgURL.RawQuery = q.Encode() + conn, err := gosql.Open("postgres", pgURL.String()) + require.NoError(t, err) + defer conn.Close() + + _, err = conn.Exec("CREATE TEMP TABLE temp_tbl()") + require.NoError(t, err) + + var errVal, sessionState, sessionRevivalToken gosql.NullString + err = conn.QueryRow("SHOW TRANSFER STATE").Scan(&errVal, &sessionState, &sessionRevivalToken) + require.NoError(t, err) + + require.True(t, errVal.Valid) + require.Equal(t, "cannot serialize session with temporary schemas", errVal.String) + require.False(t, sessionState.Valid) + require.False(t, sessionRevivalToken.Valid) + }) + }) +} diff --git a/pkg/sql/BUILD.bazel b/pkg/sql/BUILD.bazel index 8462026225d2..060d55b879bf 100644 --- a/pkg/sql/BUILD.bazel +++ b/pkg/sql/BUILD.bazel @@ -182,6 +182,7 @@ go_library( "sequence_select.go", "serial.go", "session_revival_token.go", + "session_state.go", "set_cluster_setting.go", "set_default_isolation.go", "set_schema.go", diff --git a/pkg/sql/conn_executor_exec.go b/pkg/sql/conn_executor_exec.go index fb36d8deadf8..28b27eb49ca6 100644 --- a/pkg/sql/conn_executor_exec.go +++ b/pkg/sql/conn_executor_exec.go @@ -13,6 +13,7 @@ package sql import ( "bytes" "context" + "encoding/base64" "fmt" "runtime/pprof" "strings" @@ -1652,6 +1653,8 @@ func (ex *connExecutor) runObserverStatement( return nil case *tree.ShowLastQueryStatistics: return ex.runShowLastQueryStatistics(ctx, res, sqlStmt) + case *tree.ShowTransferState: + return ex.runShowTransferState(ctx, res, sqlStmt) default: res.SetError(errors.AssertionFailedf("unrecognized observer statement type %T", ast)) return nil @@ -1689,6 +1692,97 @@ func (ex *connExecutor) runShowTransactionState( return res.AddRow(ctx, tree.Datums{tree.NewDString(state)}) } +// showTransferStateFns maps column names for the SHOW TRANSFER STATE statement +// to their generator functions. +// +// NOTE: These functions are executed in the context of an observer statement, +// and observer statements do not get planned, so a planner should not be used. +var showTransferStateFns = map[string]func(ex *connExecutor) (tree.Datum, error){ + "session_state_base64": func(ex *connExecutor) (tree.Datum, error) { + // Observer statements do not use implicit transactions at all, so + // we look at CurState() directly. + _, isNoTxn := ex.machine.CurState().(stateNoTxn) + state, err := serializeSessionState( + !isNoTxn, ex.extraTxnState.prepStmtsNamespace, ex.sessionData(), + ) + if err != nil { + return nil, err + } + return tree.NewDString(base64.StdEncoding.EncodeToString([]byte(*state))), nil + }, + "session_revival_token_base64": func(ex *connExecutor) (tree.Datum, error) { + cm, err := ex.server.cfg.RPCContext.SecurityContext.GetCertificateManager() + if err != nil { + return nil, err + } + token, err := createSessionRevivalToken( + ex.server.cfg.AllowSessionRevival, + ex.sessionData(), + cm, + ) + if err != nil { + return nil, err + } + return tree.NewDString(base64.StdEncoding.EncodeToString([]byte(*token))), nil + }, +} + +// runShowTransferState executes a SHOW TRANSFER STATE statement. +// +// If an error is returned, the connection needs to stop processing queries. +func (ex *connExecutor) runShowTransferState( + ctx context.Context, res RestrictedCommandResult, stmt *tree.ShowTransferState, +) error { + // When adding a new column, a generator function must be defined in + // showTransferStateFns. The "error" column must be the first in colNames + // if a transfer key was not defined, and second, if one was defined. + colNames := []string{ + "error", + "session_state_base64", + "session_revival_token_base64", + } + if stmt.WithTransferKey { + colNames = append([]string{"transfer_key"}, colNames...) + } + cols := make(colinfo.ResultColumns, len(colNames)) + for i := 0; i < len(colNames); i++ { + cols[i] = colinfo.ResultColumn{Name: colNames[i], Typ: types.String} + } + res.SetColumns(ctx, cols) + + row := make(tree.Datums, len(colNames)) + errIdx := 0 + if stmt.WithTransferKey { + row[0] = tree.NewDString(stmt.TransferKey) + errIdx++ + } + row[errIdx] = tree.DNull + + // When an error occurs, reset all columns (except the transfer_key column) + // to NULL, and set the error column accordingly. + finishWithError := func(err error) error { + row[errIdx] = tree.NewDString(err.Error()) + + for i := errIdx + 1; i < len(colNames); i++ { + row[i] = tree.DNull + } + return res.AddRow(ctx, row) + } + for i := errIdx + 1; i < len(colNames); i++ { + // fn must exist for the given column name. + fn, ok := showTransferStateFns[colNames[i]] + if !ok { + panic(fmt.Errorf("generator fn must exist for column '%s'", colNames[i])) + } + res, err := fn(ex) + if err != nil { + return finishWithError(err) + } + row[i] = res + } + return res.AddRow(ctx, row) +} + // showQueryStatsFns maps column names as requested by the SQL clients // to timing retrieval functions from the execution phase times. var showQueryStatsFns = map[tree.Name]func(*sessionphase.Times) time.Duration{ diff --git a/pkg/sql/conn_executor_test.go b/pkg/sql/conn_executor_test.go index 858a9d9e5f0f..90b4b4c1a941 100644 --- a/pkg/sql/conn_executor_test.go +++ b/pkg/sql/conn_executor_test.go @@ -1230,6 +1230,82 @@ CREATE TABLE t1.test (k INT PRIMARY KEY, v TEXT); }) } +// TestShowTransferStateError tests that the SHOW TRANSFER STATE statement +// will return an error as a SQL value whenever a tenant server is not used. +// For actual tests, see pkg/ccl/testccl/sqlccl. +func TestShowTransferStateError(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + + ctx := context.Background() + params := base.TestServerArgs{} + s, sqlConn, _ := serverutils.StartServer(t, params) + defer s.Stopper().Stop(ctx) + + _, err := sqlConn.Exec("SELECT 1") + require.NoError(t, err) + + t.Run("without_transfer_key", func(t *testing.T) { + rows, err := sqlConn.Query("SHOW TRANSFER STATE") + require.NoError(t, err, "show transfer state failed") + defer rows.Close() + + resultColumns, err := rows.Columns() + require.NoError(t, err) + + const expectedNumColumns = 3 + if len(resultColumns) != expectedNumColumns { + t.Fatalf( + "unexpected number of columns in result; expected %d, found %d", + expectedNumColumns, + len(resultColumns), + ) + } + + var error, sessionState, sessionRevivalToken gosql.NullString + + rows.Next() + err = rows.Scan(&error, &sessionState, &sessionRevivalToken) + require.NoError(t, err, "unexpected error while reading transfer state") + + require.True(t, error.Valid) + require.Equal(t, "session revival tokens are not supported on this cluster", error.String) + require.False(t, sessionState.Valid) + require.False(t, sessionRevivalToken.Valid) + }) + + t.Run("with_transfer_key", func(t *testing.T) { + rows, err := sqlConn.Query("SHOW TRANSFER STATE WITH 'foobar'") + require.NoError(t, err, "show transfer state failed") + defer rows.Close() + + resultColumns, err := rows.Columns() + require.NoError(t, err) + + const expectedNumColumns = 4 + if len(resultColumns) != expectedNumColumns { + t.Fatalf( + "unexpected number of columns in result; expected %d, found %d", + expectedNumColumns, + len(resultColumns), + ) + } + + var transferKey string + var error, sessionState, sessionRevivalToken gosql.NullString + + rows.Next() + err = rows.Scan(&transferKey, &error, &sessionState, &sessionRevivalToken) + require.NoError(t, err, "unexpected error while reading transfer state") + + require.Equal(t, "foobar", transferKey) + require.True(t, error.Valid) + require.Equal(t, "session revival tokens are not supported on this cluster", error.String) + require.False(t, sessionState.Valid) + require.False(t, sessionRevivalToken.Valid) + }) +} + func TestShowLastQueryStatistics(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) diff --git a/pkg/sql/delegate/delegate.go b/pkg/sql/delegate/delegate.go index c5ca17d2b13a..f869277cc35d 100644 --- a/pkg/sql/delegate/delegate.go +++ b/pkg/sql/delegate/delegate.go @@ -15,6 +15,8 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/parser" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented" ) @@ -164,7 +166,15 @@ func TryDelegate( ) case *tree.ShowSavepointStatus: - return nil, unimplemented.NewWithIssue(47333, "cannot use SHOW SAVEPOINT STATUS as a statement source") + return nil, unimplemented.NewWithIssue( + 47333, "cannot use SHOW SAVEPOINT STATUS as a statement source") + + // SHOW TRANSFER STATE cannot be rewritten as a low-level query due to + // the format of its output (e.g. transfer key echoed back, and errors are + // returned in the form of a SQL value). + case *tree.ShowTransferState: + return nil, pgerror.Newf(pgcode.FeatureNotSupported, + "cannot use SHOW TRANSFER STATE as a statement source") default: return nil, nil diff --git a/pkg/sql/faketreeeval/evalctx.go b/pkg/sql/faketreeeval/evalctx.go index 9b32b5589e5b..ec249da39e70 100644 --- a/pkg/sql/faketreeeval/evalctx.go +++ b/pkg/sql/faketreeeval/evalctx.go @@ -245,6 +245,16 @@ func (*DummyEvalPlanner) DecodeGist(gist string) ([]string, error) { return nil, errors.WithStack(errEvalPlanner) } +// SerializeSessionState is part of the EvalPlanner interface. +func (*DummyEvalPlanner) SerializeSessionState() (*tree.DBytes, error) { + return nil, errors.WithStack(errEvalPlanner) +} + +// DeserializeSessionState is part of the EvalPlanner interface. +func (*DummyEvalPlanner) DeserializeSessionState(token *tree.DBytes) (*tree.DBool, error) { + return nil, errors.WithStack(errEvalPlanner) +} + // CreateSessionRevivalToken is part of the EvalPlanner interface. func (*DummyEvalPlanner) CreateSessionRevivalToken() (*tree.DBytes, error) { return nil, errors.WithStack(errEvalPlanner) diff --git a/pkg/sql/logictest/testdata/logic_test/show_source b/pkg/sql/logictest/testdata/logic_test/show_source index 06d32f65c6f6..4bd4e277a6fa 100644 --- a/pkg/sql/logictest/testdata/logic_test/show_source +++ b/pkg/sql/logictest/testdata/logic_test/show_source @@ -460,6 +460,12 @@ file lexer.go function Error detail source SQL: foo ^ +# Test the SHOW TRANSFER STATE statement. +statement error pgcode 0A000 cannot use SHOW TRANSFER STATE as a statement source +SELECT * FROM [SHOW TRANSFER STATE] + +statement error pgcode 0A000 cannot use SHOW TRANSFER STATE as a statement source +SELECT * FROM [SHOW TRANSFER STATE WITH 'foo'] # Test the SHOW INDEXES FROM DATABASE COMMAND statement ok diff --git a/pkg/sql/parser/help_test.go b/pkg/sql/parser/help_test.go index 30652fad37e5..eafb84b51f70 100644 --- a/pkg/sql/parser/help_test.go +++ b/pkg/sql/parser/help_test.go @@ -369,6 +369,11 @@ func TestContextualHelp(t *testing.T) { {`SHOW SYNTAX 'foo' ??`, `SHOW SYNTAX`}, {`SHOW SAVEPOINT STATUS ??`, `SHOW SAVEPOINT`}, + {`SHOW TRANSFER ??`, `SHOW TRANSFER`}, + {`SHOW TRANSFER STATE ??`, `SHOW TRANSFER`}, + {`SHOW TRANSFER STATE WITH ??`, `SHOW TRANSFER`}, + {`SHOW TRANSFER STATE WITH foo ??`, `SHOW TRANSFER`}, + {`SHOW RANGE ??`, `SHOW RANGE`}, {`SHOW RANGES ??`, `SHOW RANGES`}, diff --git a/pkg/sql/parser/sql.y b/pkg/sql/parser/sql.y index 7602461ab836..2e4cae2463d1 100644 --- a/pkg/sql/parser/sql.y +++ b/pkg/sql/parser/sql.y @@ -849,12 +849,12 @@ func (u *sqlSymUnion) setVar() *tree.SetVar { %token SKIP_MISSING_SEQUENCES SKIP_MISSING_SEQUENCE_OWNERS SKIP_MISSING_VIEWS SMALLINT SMALLSERIAL SNAPSHOT SOME SPLIT SQL %token SQLLOGIN -%token START STATISTICS STATUS STDIN STREAM STRICT STRING STORAGE STORE STORED STORING SUBSTRING +%token START STATE STATISTICS STATUS STDIN STREAM STRICT STRING STORAGE STORE STORED STORING SUBSTRING %token SURVIVE SURVIVAL SYMMETRIC SYNTAX SYSTEM SQRT SUBSCRIPTION STATEMENTS %token TABLE TABLES TABLESPACE TEMP TEMPLATE TEMPORARY TENANT TESTING_RELOCATE TEXT THEN %token TIES TIME TIMETZ TIMESTAMP TIMESTAMPTZ TO THROTTLING TRAILING TRACE -%token TRANSACTION TRANSACTIONS TREAT TRIGGER TRIM TRUE +%token TRANSACTION TRANSACTIONS TRANSFER TREAT TRIGGER TRIM TRUE %token TRUNCATE TRUSTED TYPE TYPES %token TRACING @@ -1096,6 +1096,7 @@ func (u *sqlSymUnion) setVar() *tree.SetVar { %type show_trace_stmt %type show_transaction_stmt %type show_transactions_stmt +%type show_transfer_stmt %type show_types_stmt %type show_users_stmt %type show_zone_stmt @@ -5039,8 +5040,8 @@ zone_value: // PARTITIONS, SHOW JOBS, SHOW STATEMENTS, SHOW RANGE, SHOW RANGES, SHOW REGIONS, SHOW SURVIVAL GOAL, // SHOW ROLES, SHOW SCHEMAS, SHOW SEQUENCES, SHOW SESSION, SHOW SESSIONS, // SHOW STATISTICS, SHOW SYNTAX, SHOW TABLES, SHOW TRACE, SHOW TRANSACTION, -// SHOW TRANSACTIONS, SHOW TYPES, SHOW USERS, SHOW LAST QUERY STATISTICS, SHOW SCHEDULES, -// SHOW LOCALITY, SHOW ZONE CONFIGURATION, SHOW FULL TABLE SCANS +// SHOW TRANSACTIONS, SHOW TRANSFER, SHOW TYPES, SHOW USERS, SHOW LAST QUERY STATISTICS, +// SHOW SCHEDULES, SHOW LOCALITY, SHOW ZONE CONFIGURATION, SHOW FULL TABLE SCANS show_stmt: show_backup_stmt // EXTEND WITH HELP: SHOW BACKUP | show_columns_stmt // EXTEND WITH HELP: SHOW COLUMNS @@ -5076,6 +5077,7 @@ show_stmt: | show_trace_stmt // EXTEND WITH HELP: SHOW TRACE | show_transaction_stmt // EXTEND WITH HELP: SHOW TRANSACTION | show_transactions_stmt // EXTEND WITH HELP: SHOW TRANSACTIONS +| show_transfer_stmt // EXTEND WITH HELP: SHOW TRANSFER | show_users_stmt // EXTEND WITH HELP: SHOW USERS | show_zone_stmt // EXTEND WITH HELP: SHOW ZONE CONFIGURATION | SHOW error // SHOW HELP: SHOW @@ -5797,6 +5799,20 @@ show_transaction_stmt: } | SHOW TRANSACTION error // SHOW HELP: SHOW TRANSACTION +// %Help: SHOW TRANSFER - display current transfer properties +// %Category: Cfg +// %Text: SHOW TRANSFER STATE [ WITH '' ] +show_transfer_stmt: + SHOW TRANSFER STATE WITH non_reserved_word_or_sconst + { + $$.val = &tree.ShowTransferState{WithTransferKey: true, TransferKey: $5} + } +| SHOW TRANSFER STATE + { + $$.val = &tree.ShowTransferState{} + } +| SHOW TRANSFER error // SHOW HELP: SHOW TRANSFER + // %Help: SHOW CREATE - display the CREATE statement for a table, sequence, view, or database // %Category: DDL // %Text: @@ -13671,6 +13687,7 @@ unreserved_keyword: | SQL | SQLLOGIN | START +| STATE | STATEMENTS | STATISTICS | STDIN @@ -13697,6 +13714,7 @@ unreserved_keyword: | TRACE | TRANSACTION | TRANSACTIONS +| TRANSFER | TRIGGER | TRUNCATE | TRUSTED diff --git a/pkg/sql/parser/testdata/show b/pkg/sql/parser/testdata/show index ac4560e4cba3..8dcf22d56db3 100644 --- a/pkg/sql/parser/testdata/show +++ b/pkg/sql/parser/testdata/show @@ -1507,7 +1507,29 @@ EXPLAIN SHOW SAVEPOINT STATUS -- fully parenthesized EXPLAIN SHOW SAVEPOINT STATUS -- literals removed EXPLAIN SHOW SAVEPOINT STATUS -- identifiers removed +parse +SHOW TRANSFER STATE +---- +SHOW TRANSFER STATE +SHOW TRANSFER STATE -- fully parenthesized +SHOW TRANSFER STATE -- literals removed +SHOW TRANSFER STATE -- identifiers removed +parse +SHOW TRANSFER STATE WITH 'foo' +---- +SHOW TRANSFER STATE WITH 'foo' +SHOW TRANSFER STATE WITH 'foo' -- fully parenthesized +SHOW TRANSFER STATE WITH 'foo' -- literals removed +SHOW TRANSFER STATE WITH 'foo' -- identifiers removed + +parse +SHOW TRANSFER STATE WITH foo +---- +SHOW TRANSFER STATE WITH 'foo' -- normalized! +SHOW TRANSFER STATE WITH 'foo' -- fully parenthesized +SHOW TRANSFER STATE WITH 'foo' -- literals removed +SHOW TRANSFER STATE WITH 'foo' -- identifiers removed parse SHOW LAST QUERY STATISTICS diff --git a/pkg/sql/sem/builtins/builtins.go b/pkg/sql/sem/builtins/builtins.go index a63eec02c4c5..e0f9c9d9bfb2 100644 --- a/pkg/sql/sem/builtins/builtins.go +++ b/pkg/sql/sem/builtins/builtins.go @@ -76,7 +76,6 @@ import ( "github.com/cockroachdb/cockroach/pkg/util/ipaddr" "github.com/cockroachdb/cockroach/pkg/util/json" "github.com/cockroachdb/cockroach/pkg/util/log" - "github.com/cockroachdb/cockroach/pkg/util/protoutil" "github.com/cockroachdb/cockroach/pkg/util/timeofday" "github.com/cockroachdb/cockroach/pkg/util/timetz" "github.com/cockroachdb/cockroach/pkg/util/timeutil" @@ -6309,46 +6308,7 @@ table's zone configuration this will return NULL.`, Types: tree.ArgTypes{}, ReturnType: tree.FixedReturnType(types.Bytes), Fn: func(evalCtx *tree.EvalContext, args tree.Datums) (tree.Datum, error) { - if !evalCtx.TxnImplicit { - return nil, pgerror.Newf( - pgcode.InvalidTransactionState, - "cannot serialize a session which is inside a transaction", - ) - } - - if evalCtx.PreparedStatementState.HasPrepared() { - return nil, pgerror.Newf( - pgcode.InvalidTransactionState, - "cannot serialize a session which has portals or prepared statements", - ) - } - - sd := evalCtx.SessionData() - if sd == nil { - return nil, pgerror.Newf( - pgcode.InvalidTransactionState, - "no session is active", - ) - } - - if len(sd.DatabaseIDToTempSchemaID) > 0 { - return nil, pgerror.Newf( - pgcode.InvalidTransactionState, - "cannot serialize session with temporary schemas", - ) - } - - var m sessiondatapb.MigratableSession - m.SessionData = sd.SessionData - sessiondata.MarshalNonLocal(sd, &m.SessionData) - m.LocalOnlySessionData = sd.LocalOnlySessionData - - b, err := protoutil.Marshal(&m) - if err != nil { - return nil, err - } - - return tree.NewDBytes(tree.DBytes(b)), nil + return evalCtx.Planner.SerializeSessionState() }, Info: `This function serializes the variables in the current session.`, Volatility: tree.VolatilityVolatile, @@ -6363,38 +6323,8 @@ table's zone configuration this will return NULL.`, Types: tree.ArgTypes{{"session", types.Bytes}}, ReturnType: tree.FixedReturnType(types.Bool), Fn: func(evalCtx *tree.EvalContext, args tree.Datums) (tree.Datum, error) { - if !evalCtx.TxnImplicit { - return nil, pgerror.Newf( - pgcode.InvalidTransactionState, - "cannot deserialize a session whilst inside a transaction", - ) - } - - var m sessiondatapb.MigratableSession - if err := protoutil.Unmarshal([]byte(tree.MustBeDBytes(args[0])), &m); err != nil { - return nil, pgerror.WithCandidateCode( - errors.Wrapf(err, "error deserializing session"), - pgcode.InvalidParameterValue, - ) - } - sd, err := sessiondata.UnmarshalNonLocal(m.SessionData) - if err != nil { - return nil, err - } - sd.SessionData = m.SessionData - sd.LocalUnmigratableSessionData = evalCtx.SessionData().LocalUnmigratableSessionData - sd.LocalOnlySessionData = m.LocalOnlySessionData - if sd.SessionUser().Normalized() != evalCtx.SessionData().SessionUser().Normalized() { - return nil, pgerror.Newf( - pgcode.InsufficientPrivilege, - "can only deserialize matching session users", - ) - } - if err := evalCtx.Planner.CheckCanBecomeUser(evalCtx.Context, sd.User()); err != nil { - return nil, err - } - *evalCtx.SessionData() = *sd - return tree.MakeDBool(true), nil + state := tree.MustBeDBytes(args[0]) + return evalCtx.Planner.DeserializeSessionState(&state) }, Info: `This function deserializes the serialized variables into the current session.`, Volatility: tree.VolatilityVolatile, diff --git a/pkg/sql/sem/tree/eval.go b/pkg/sql/sem/tree/eval.go index 0ae672c33b53..ebfd3feadc90 100644 --- a/pkg/sql/sem/tree/eval.go +++ b/pkg/sql/sem/tree/eval.go @@ -3263,6 +3263,14 @@ type EvalPlanner interface { // DecodeGist exposes gist functionality to the builtin functions. DecodeGist(gist string) ([]string, error) + // SerializeSessionState serializes the variables in the current session + // and returns a state, in bytes form. + SerializeSessionState() (*DBytes, error) + + // DeserializeSessionState deserializes the state as serialized variables + // into the current session. + DeserializeSessionState(state *DBytes) (*DBool, error) + // CreateSessionRevivalToken creates a token that can be used to log in // as the current user, in bytes form. CreateSessionRevivalToken() (*DBytes, error) diff --git a/pkg/sql/sem/tree/show.go b/pkg/sql/sem/tree/show.go index 51324b69b6eb..9bb99b774795 100644 --- a/pkg/sql/sem/tree/show.go +++ b/pkg/sql/sem/tree/show.go @@ -885,3 +885,22 @@ func (n *ShowDefaultPrivileges) Format(ctx *FmtCtx) { ctx.WriteString("FOR ALL ROLES ") } } + +// ShowTransferState represents a SHOW TRANSFER STATE statement. +type ShowTransferState struct { + // WithTransferKey indicates that "WITH " was used in the + // statement. This allows an empty string to be passed as a transfer key if + // necessary. + WithTransferKey bool + TransferKey string +} + +// Format implements the NodeFormatter interface. +func (node *ShowTransferState) Format(ctx *FmtCtx) { + ctx.WriteString("SHOW TRANSFER STATE") + if node.WithTransferKey { + ctx.WriteString(" WITH '") + ctx.WriteString(node.TransferKey) + ctx.WriteString("'") + } +} diff --git a/pkg/sql/sem/tree/stmt.go b/pkg/sql/sem/tree/stmt.go index 262361a07dd9..d6a0f03ee954 100644 --- a/pkg/sql/sem/tree/stmt.go +++ b/pkg/sql/sem/tree/stmt.go @@ -1423,6 +1423,17 @@ func (*ShowTransactionStatus) StatementTag() string { return "SHOW TRANSACTION S func (*ShowTransactionStatus) observerStatement() {} +// StatementReturnType implements the Statement interface. +func (*ShowTransferState) StatementReturnType() StatementReturnType { return Rows } + +// StatementType implements the Statement interface. +func (*ShowTransferState) StatementType() StatementType { return TypeDML } + +// StatementTag returns a short string identifying the type of statement. +func (*ShowTransferState) StatementTag() string { return "SHOW TRANSFER STATE" } + +func (*ShowTransferState) observerStatement() {} + // StatementReturnType implements the Statement interface. func (*ShowSavepointStatus) StatementReturnType() StatementReturnType { return Rows } @@ -1794,6 +1805,7 @@ func (n *ShowTypes) String() string { return AsString(n) } func (n *ShowTraceForSession) String() string { return AsString(n) } func (n *ShowTransactionStatus) String() string { return AsString(n) } func (n *ShowTransactions) String() string { return AsString(n) } +func (n *ShowTransferState) String() string { return AsString(n) } func (n *ShowUsers) String() string { return AsString(n) } func (n *ShowVar) String() string { return AsString(n) } func (n *ShowZoneConfig) String() string { return AsString(n) } diff --git a/pkg/sql/session_revival_token.go b/pkg/sql/session_revival_token.go index f220099d0dd4..1d6af2736e46 100644 --- a/pkg/sql/session_revival_token.go +++ b/pkg/sql/session_revival_token.go @@ -11,28 +11,45 @@ package sql import ( + "github.com/cockroachdb/cockroach/pkg/security" "github.com/cockroachdb/cockroach/pkg/security/sessionrevival" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/sessiondata" ) +// CreateSessionRevivalToken is a wrapper for createSessionRevivalToken, and +// uses the planner. func (p *planner) CreateSessionRevivalToken() (*tree.DBytes, error) { - if !p.ExecCfg().AllowSessionRevival { + cm, err := p.ExecCfg().RPCContext.SecurityContext.GetCertificateManager() + if err != nil { + return nil, err + } + return createSessionRevivalToken(p.ExecCfg().AllowSessionRevival, p.SessionData(), cm) +} + +// createSessionRevivalToken creates a session revival token for the current +// user. +// +// NOTE: This is used within an observer statement directly, and should not rely +// on the planner because those statements do not get planned. +func createSessionRevivalToken( + allowSessionRevival bool, sd *sessiondata.SessionData, cm *security.CertificateManager, +) (*tree.DBytes, error) { + if !allowSessionRevival { return nil, pgerror.New(pgcode.FeatureNotSupported, "session revival tokens are not supported on this cluster") } + // Note that we use SessionUser here and not CurrentUser, since when the // token is used to create a new session, it should be for the user who was // originally authenticated. (Whereas CurrentUser could be a different user // if SET ROLE had been used.) - user := p.SessionData().SessionUser() + user := sd.SessionUser() if user.IsRootUser() { return nil, pgerror.New(pgcode.InsufficientPrivilege, "cannot create token for root user") } - cm, err := p.ExecCfg().RPCContext.SecurityContext.GetCertificateManager() - if err != nil { - return nil, err - } + tokenBytes, err := sessionrevival.CreateSessionRevivalToken(cm, user) if err != nil { return nil, err @@ -40,6 +57,7 @@ func (p *planner) CreateSessionRevivalToken() (*tree.DBytes, error) { return tree.NewDBytes(tree.DBytes(tokenBytes)), nil } +// ValidateSessionRevivalToken validates a session revival token. func (p *planner) ValidateSessionRevivalToken(token *tree.DBytes) (*tree.DBool, error) { if !p.ExecCfg().AllowSessionRevival { return nil, pgerror.New(pgcode.FeatureNotSupported, "session revival tokens are not supported on this cluster") diff --git a/pkg/sql/session_state.go b/pkg/sql/session_state.go new file mode 100644 index 000000000000..789f04e99609 --- /dev/null +++ b/pkg/sql/session_state.go @@ -0,0 +1,119 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package sql + +import ( + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/sessiondata" + "github.com/cockroachdb/cockroach/pkg/sql/sessiondatapb" + "github.com/cockroachdb/cockroach/pkg/util/protoutil" + "github.com/cockroachdb/errors" +) + +// SerializeSessionState is a wrapper for serializeSessionState, and uses the +// planner. +func (p *planner) SerializeSessionState() (*tree.DBytes, error) { + evalCtx := p.EvalContext() + return serializeSessionState( + !evalCtx.TxnImplicit, + evalCtx.PreparedStatementState, + p.SessionData(), + ) +} + +// serializeSessionState serializes the current session's state into bytes. +// +// NOTE: This is used within an observer statement directly, and should not rely +// on the planner because those statements do not get planned. +func serializeSessionState( + inTxn bool, prepStmtsState tree.PreparedStatementState, sd *sessiondata.SessionData, +) (*tree.DBytes, error) { + if inTxn { + return nil, pgerror.Newf( + pgcode.InvalidTransactionState, + "cannot serialize a session which is inside a transaction", + ) + } + + if prepStmtsState.HasPrepared() { + return nil, pgerror.Newf( + pgcode.InvalidTransactionState, + "cannot serialize a session which has portals or prepared statements", + ) + } + + if sd == nil { + return nil, pgerror.Newf( + pgcode.InvalidTransactionState, + "no session is active", + ) + } + + if len(sd.DatabaseIDToTempSchemaID) > 0 { + return nil, pgerror.Newf( + pgcode.InvalidTransactionState, + "cannot serialize session with temporary schemas", + ) + } + + var m sessiondatapb.MigratableSession + m.SessionData = sd.SessionData + sessiondata.MarshalNonLocal(sd, &m.SessionData) + m.LocalOnlySessionData = sd.LocalOnlySessionData + + b, err := protoutil.Marshal(&m) + if err != nil { + return nil, err + } + + return tree.NewDBytes(tree.DBytes(b)), nil +} + +// DeserializeSessionState deserializes the given state into the current session. +func (p *planner) DeserializeSessionState(state *tree.DBytes) (*tree.DBool, error) { + evalCtx := p.EvalContext() + + if !evalCtx.TxnImplicit { + return nil, pgerror.Newf( + pgcode.InvalidTransactionState, + "cannot deserialize a session whilst inside a transaction", + ) + } + + var m sessiondatapb.MigratableSession + if err := protoutil.Unmarshal([]byte(*state), &m); err != nil { + return nil, pgerror.WithCandidateCode( + errors.Wrapf(err, "error deserializing session"), + pgcode.InvalidParameterValue, + ) + } + sd, err := sessiondata.UnmarshalNonLocal(m.SessionData) + if err != nil { + return nil, err + } + sd.SessionData = m.SessionData + sd.LocalUnmigratableSessionData = evalCtx.SessionData().LocalUnmigratableSessionData + sd.LocalOnlySessionData = m.LocalOnlySessionData + if sd.SessionUser().Normalized() != evalCtx.SessionData().SessionUser().Normalized() { + return nil, pgerror.Newf( + pgcode.InsufficientPrivilege, + "can only deserialize matching session users", + ) + } + if err := p.CheckCanBecomeUser(evalCtx.Context, sd.User()); err != nil { + return nil, err + } + *p.SessionData() = *sd + + return tree.MakeDBool(true), nil +}