From 3fdbc14da4e2e24d68f1d17ae8c8d8805a3e2039 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 Co-authored-by: Jay Co-authored-by: Rafi Shamim --- 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 | 254 ++++++++++++++++++ pkg/gen/docs.bzl | 1 + pkg/sql/BUILD.bazel | 1 + pkg/sql/conn_executor_exec.go | 89 ++++++ pkg/sql/delegate/delegate.go | 12 +- pkg/sql/faketreeeval/evalctx.go | 10 + .../logictest/testdata/logic_test/show_source | 6 + .../testdata/logic_test/show_transfer_state | 14 + pkg/sql/parser/help_test.go | 5 + pkg/sql/parser/sql.y | 26 +- pkg/sql/parser/testdata/show | 38 +++ pkg/sql/sem/builtins/builtins.go | 76 +----- pkg/sql/sem/tree/eval.go | 8 + pkg/sql/sem/tree/show.go | 14 + pkg/sql/sem/tree/stmt.go | 12 + pkg/sql/session_revival_token.go | 30 ++- pkg/sql/session_state.go | 119 ++++++++ 22 files changed, 645 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/logictest/testdata/logic_test/show_transfer_state 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 e904ae88c2b7..def1c26d5e56 100644 --- a/docs/generated/sql/bnf/BUILD.bazel +++ b/docs/generated/sql/bnf/BUILD.bazel @@ -206,6 +206,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..bdf416bb6799 --- /dev/null +++ b/docs/generated/sql/bnf/show_transfer_stmt.bnf @@ -0,0 +1,3 @@ +show_transfer_stmt ::= + 'SHOW' 'TRANSFER' 'STATE' 'WITH' '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 cb000bfc2ebc..2dc54461664e 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 @@ -779,6 +780,10 @@ show_transactions_stmt ::= 'SHOW' opt_cluster 'TRANSACTIONS' | 'SHOW' 'ALL' opt_cluster 'TRANSACTIONS' +show_transfer_stmt ::= + 'SHOW' 'TRANSFER' 'STATE' 'WITH' 'SCONST' + | 'SHOW' 'TRANSFER' 'STATE' + show_users_stmt ::= 'SHOW' 'USERS' @@ -1157,6 +1162,7 @@ unreserved_keyword ::= | 'SQL' | 'SQLLOGIN' | 'START' + | 'STATE' | 'STATEMENTS' | 'STATISTICS' | 'STDIN' @@ -1183,6 +1189,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..5a4c2a8a024d --- /dev/null +++ b/pkg/ccl/testccl/sqlccl/show_transfer_state_test.go @@ -0,0 +1,254 @@ +// 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) + + require.Equal(t, []string{ + "error", + "session_state_base64", + "session_revival_token_base64", + }, 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) + + require.Equal(t, []string{ + "error", + "session_state_base64", + "session_revival_token_base64", + "transfer_key", + }, resultColumns) + + var key string + var errVal, sessionState, sessionRevivalToken gosql.NullString + + rows.Next() + err = rows.Scan(&errVal, &sessionState, &sessionRevivalToken, &key) + 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(&errVal, &sessionState, &sessionRevivalToken, &key) + 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/gen/docs.bzl b/pkg/gen/docs.bzl index e033c0a4a6ba..3e1f61ada732 100644 --- a/pkg/gen/docs.bzl +++ b/pkg/gen/docs.bzl @@ -218,6 +218,7 @@ DOCS_SRCS = [ "//docs/generated/sql/bnf:show_tables.bnf", "//docs/generated/sql/bnf:show_trace.bnf", "//docs/generated/sql/bnf:show_transactions_stmt.bnf", + "//docs/generated/sql/bnf:show_transfer_stmt.bnf", "//docs/generated/sql/bnf:show_types_stmt.bnf", "//docs/generated/sql/bnf:show_users_stmt.bnf", "//docs/generated/sql/bnf:show_var.bnf", diff --git a/pkg/sql/BUILD.bazel b/pkg/sql/BUILD.bazel index 646718fac821..3c21498464ac 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 a0a86e80865e..ca5b46441a33 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" @@ -1653,6 +1654,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 @@ -1690,6 +1693,92 @@ func (ex *connExecutor) runShowTransactionState( return res.AddRow(ctx, tree.Datums{tree.NewDString(state)}) } +// sessionStateBase64 returns the serialized session state in a base64 form. +// See runShowTransferState for more information. +// +// Note: Do not use a planner here because this is used in the context of an +// observer statement. +func (ex *connExecutor) sessionStateBase64() (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 +} + +// sessionRevivalTokenBase64 creates a session revival token and returns it in +// a base64 form. See runShowTransferState for more information. +// +// Note: Do not use a planner here because this is used in the context of an +// observer statement. +func (ex *connExecutor) sessionRevivalTokenBase64() (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 { + // The transfer_key column must always be the last. + colNames := []string{ + "error", "session_state_base64", "session_revival_token_base64", + } + if stmt.TransferKey != nil { + colNames = append(colNames, "transfer_key") + } + 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) + + var sessionState, sessionRevivalToken tree.Datum + var row tree.Datums + err := func() error { + // 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 err error + if sessionState, err = ex.sessionStateBase64(); err != nil { + return err + } + if sessionRevivalToken, err = ex.sessionRevivalTokenBase64(); err != nil { + return err + } + return nil + }() + if err != nil { + // When an error occurs, only show the error column (plus transfer_key + // column if it exists), and NULL for everything else. + row = []tree.Datum{tree.NewDString(err.Error()), tree.DNull, tree.DNull} + } else { + row = []tree.Datum{tree.DNull, sessionState, sessionRevivalToken} + } + if stmt.TransferKey != nil { + row = append(row, tree.NewDString(stmt.TransferKey.RawString())) + } + 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/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 8f110f69d00f..7d0d99ced971 100644 --- a/pkg/sql/logictest/testdata/logic_test/show_source +++ b/pkg/sql/logictest/testdata/logic_test/show_source @@ -462,6 +462,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-bar'] # Test the SHOW INDEXES FROM DATABASE COMMAND statement ok diff --git a/pkg/sql/logictest/testdata/logic_test/show_transfer_state b/pkg/sql/logictest/testdata/logic_test/show_transfer_state new file mode 100644 index 000000000000..51971cd36dae --- /dev/null +++ b/pkg/sql/logictest/testdata/logic_test/show_transfer_state @@ -0,0 +1,14 @@ +# LogicTest: !3node-tenant + +# Statement does not work on system tenant. +query TTT colnames +SHOW TRANSFER STATE +---- +error session_state_base64 session_revival_token_base64 +session revival tokens are not supported on this cluster NULL NULL + +query TTTT colnames +SHOW TRANSFER STATE WITH 'foo-bar' +---- +error session_state_base64 session_revival_token_base64 transfer_key +session revival tokens are not supported on this cluster NULL NULL foo-bar diff --git a/pkg/sql/parser/help_test.go b/pkg/sql/parser/help_test.go index 9be48874d9f2..7aacece0ee91 100644 --- a/pkg/sql/parser/help_test.go +++ b/pkg/sql/parser/help_test.go @@ -373,6 +373,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 834e6e8ca482..281a5d7cbedc 100644 --- a/pkg/sql/parser/sql.y +++ b/pkg/sql/parser/sql.y @@ -858,12 +858,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 @@ -1106,6 +1106,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 @@ -5093,8 +5094,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 @@ -5130,6 +5131,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 @@ -5851,6 +5853,20 @@ show_transaction_stmt: } | SHOW TRANSACTION error // SHOW HELP: SHOW TRANSACTION +// %Help: SHOW TRANSFER - display session state for connection migration +// %Category: Misc +// %Text: SHOW TRANSFER STATE [ WITH '' ] +show_transfer_stmt: + SHOW TRANSFER STATE WITH SCONST + { + $$.val = &tree.ShowTransferState{TransferKey: tree.NewStrVal($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: @@ -13725,6 +13741,7 @@ unreserved_keyword: | SQL | SQLLOGIN | START +| STATE | STATEMENTS | STATISTICS | STDIN @@ -13751,6 +13768,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..44d84d143057 100644 --- a/pkg/sql/parser/testdata/show +++ b/pkg/sql/parser/testdata/show @@ -1507,7 +1507,45 @@ 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 '' +---- +SHOW TRANSFER STATE WITH '' +SHOW TRANSFER STATE WITH ('') -- fully parenthesized +SHOW TRANSFER STATE WITH '_' -- literals removed +SHOW TRANSFER STATE WITH '' -- identifiers removed + +parse +SHOW TRANSFER STATE WITH 'foo-bar' +---- +SHOW TRANSFER STATE WITH 'foo-bar' +SHOW TRANSFER STATE WITH ('foo-bar') -- fully parenthesized +SHOW TRANSFER STATE WITH '_' -- literals removed +SHOW TRANSFER STATE WITH 'foo-bar' -- identifiers removed + +parse +SHOW TRANSFER STATE WITH 'foo' +---- +SHOW TRANSFER STATE WITH 'foo' +SHOW TRANSFER STATE WITH ('foo') -- fully parenthesized +SHOW TRANSFER STATE WITH '_' -- literals removed +SHOW TRANSFER STATE WITH 'foo' -- identifiers removed + +parse +SHOW TRANSFER STATE WITH 'foo''o' +---- +SHOW TRANSFER STATE WITH e'foo\'o' -- normalized! +SHOW TRANSFER STATE WITH (e'foo\'o') -- fully parenthesized +SHOW TRANSFER STATE WITH '_' -- literals removed +SHOW TRANSFER STATE WITH e'foo\'o' -- identifiers removed parse SHOW LAST QUERY STATISTICS diff --git a/pkg/sql/sem/builtins/builtins.go b/pkg/sql/sem/builtins/builtins.go index 4b96fc3a3ac5..64f592c6f8fc 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" @@ -6330,46 +6329,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, @@ -6384,38 +6344,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 3523182b2a44..3c18c07b5892 100644 --- a/pkg/sql/sem/tree/eval.go +++ b/pkg/sql/sem/tree/eval.go @@ -3271,6 +3271,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 1884fca17c13..fe05d973aea2 100644 --- a/pkg/sql/sem/tree/show.go +++ b/pkg/sql/sem/tree/show.go @@ -892,3 +892,17 @@ func (n *ShowDefaultPrivileges) Format(ctx *FmtCtx) { ctx.WriteString("FOR ALL ROLES ") } } + +// ShowTransferState represents a SHOW TRANSFER STATE statement. +type ShowTransferState struct { + TransferKey *StrVal +} + +// Format implements the NodeFormatter interface. +func (node *ShowTransferState) Format(ctx *FmtCtx) { + ctx.WriteString("SHOW TRANSFER STATE") + if node.TransferKey != nil { + ctx.WriteString(" WITH ") + ctx.FormatNode(node.TransferKey) + } +} diff --git a/pkg/sql/sem/tree/stmt.go b/pkg/sql/sem/tree/stmt.go index 508b05b51d6b..c2252d742db2 100644 --- a/pkg/sql/sem/tree/stmt.go +++ b/pkg/sql/sem/tree/stmt.go @@ -1435,6 +1435,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 } @@ -1808,6 +1819,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 +}