diff --git a/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go b/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go index 87807cfed3d0..52ec69440b45 100644 --- a/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go +++ b/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go @@ -1410,6 +1410,13 @@ func TestTenantLogic_propagate_input_ordering( runLogicTest(t, "propagate_input_ordering") } +func TestTenantLogic_raise( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "raise") +} + func TestTenantLogic_reassign_owned_by( t *testing.T, ) { diff --git a/pkg/sql/conn_io.go b/pkg/sql/conn_io.go index aab10ba50c18..8087f37ad21d 100644 --- a/pkg/sql/conn_io.go +++ b/pkg/sql/conn_io.go @@ -818,6 +818,9 @@ type RestrictedCommandResult interface { // This gets flushed only when the CommandResult is closed. BufferNotice(notice pgnotice.Notice) + // SendNotice immediately flushes a notice to the client. + SendNotice(ctx context.Context, notice pgnotice.Notice) error + // SetColumns informs the client about the schema of the result. The columns // can be nil. // @@ -1079,6 +1082,12 @@ func (r *streamingCommandResult) BufferNotice(notice pgnotice.Notice) { // Unimplemented: the internal executor does not support notices. } +// SendNotice is part of the RestrictedCommandResult interface. +func (r *streamingCommandResult) SendNotice(ctx context.Context, notice pgnotice.Notice) error { + // Unimplemented: the internal executor does not support notices. + return nil +} + // ResetStmtType is part of the RestrictedCommandResult interface. func (r *streamingCommandResult) ResetStmtType(stmt tree.Statement) { panic("unimplemented") diff --git a/pkg/sql/faketreeeval/evalctx.go b/pkg/sql/faketreeeval/evalctx.go index 18c2816acf54..26b6a9a19fa3 100644 --- a/pkg/sql/faketreeeval/evalctx.go +++ b/pkg/sql/faketreeeval/evalctx.go @@ -586,6 +586,11 @@ var _ eval.ClientNoticeSender = &DummyClientNoticeSender{} // BufferClientNotice is part of the eval.ClientNoticeSender interface. func (c *DummyClientNoticeSender) BufferClientNotice(context.Context, pgnotice.Notice) {} +// SendClientNotice is part of the eval.ClientNoticeSender interface. +func (c *DummyClientNoticeSender) SendClientNotice(context.Context, pgnotice.Notice) error { + return nil +} + // DummyTenantOperator implements the tree.TenantOperator interface. type DummyTenantOperator struct{} diff --git a/pkg/sql/logictest/logic.go b/pkg/sql/logictest/logic.go index 71082e36af52..c11faafbfb01 100644 --- a/pkg/sql/logictest/logic.go +++ b/pkg/sql/logictest/logic.go @@ -1248,6 +1248,9 @@ func (t *logicTest) openDB(pgURL url.URL) *gosql.DB { if notice.Hint != "" { t.noticeBuffer = append(t.noticeBuffer, "HINT: "+notice.Hint) } + if notice.Code != "" && notice.Code != "00000" { + t.noticeBuffer = append(t.noticeBuffer, "SQLSTATE: "+string(notice.Code)) + } }) return gosql.OpenDB(connector) diff --git a/pkg/sql/logictest/testdata/logic_test/notice b/pkg/sql/logictest/testdata/logic_test/notice index 9cf9650645cf..b2527accfad4 100644 --- a/pkg/sql/logictest/testdata/logic_test/notice +++ b/pkg/sql/logictest/testdata/logic_test/notice @@ -57,4 +57,5 @@ UNLISTEN temp ---- NOTICE: unimplemented: CRDB does not support LISTEN, making UNLISTEN a no-op HINT: You have attempted to use a feature that is not yet implemented. -See: https://go.crdb.dev/issue-v/41522/* +See: https://go.crdb.dev/issue-v/41522/v23.2 +SQLSTATE: 0A000 diff --git a/pkg/sql/logictest/testdata/logic_test/raise b/pkg/sql/logictest/testdata/logic_test/raise new file mode 100644 index 000000000000..b53d5aa28f72 --- /dev/null +++ b/pkg/sql/logictest/testdata/logic_test/raise @@ -0,0 +1,166 @@ +# Test different log levels. +query T noticetrace +SELECT crdb_internal.plpgsql_raise('DEBUG1', 'foo', '', '', ''); +---- + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('LOG', 'foo', '', '', ''); +---- + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('INFO', 'foo', '', '', ''); +---- +INFO: foo +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', 'foo', '', '', ''); +---- +NOTICE: foo +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('WARNING', 'foo', '', '', ''); +---- +WARNING: foo +SQLSTATE: XXUUU + +statement ok +SET client_min_messages = 'debug1'; + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('DEBUG1', 'foo', '', '', ''); +---- +DEBUG1: foo +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('LOG', 'foo', '', '', ''); +---- +LOG: foo +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('INFO', 'foo', '', '', ''); +---- +INFO: foo +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', 'foo', '', '', ''); +---- +NOTICE: foo +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('WARNING', 'foo', '', '', ''); +---- +WARNING: foo +SQLSTATE: XXUUU + +statement ok +SET client_min_messages = 'WARNING'; + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('DEBUG1', 'foo', '', '', ''); +---- + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('LOG', 'foo', '', '', ''); +---- + +# INFO-level notices are always sent to the client. +query T noticetrace +SELECT crdb_internal.plpgsql_raise('INFO', 'foo', '', '', ''); +---- +INFO: foo +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', 'foo', '', '', ''); +---- + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('WARNING', 'foo', '', '', ''); +---- +WARNING: foo +SQLSTATE: XXUUU + +statement ok +RESET client_min_messages; + +# Test RAISE options. +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', 'bar', 'this is a detail', '', ''); +---- +NOTICE: bar +DETAIL: this is a detail +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', 'baz', '', 'this is a hint', ''); +---- +NOTICE: baz +HINT: this is a hint +SQLSTATE: XXUUU + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', 'division by zero', '', '', '22012'); +---- +NOTICE: division by zero +SQLSTATE: 22012 + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('WARNING', 'invalid password', '', '', '28P01'); +---- +WARNING: invalid password +SQLSTATE: 28P01 + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', 'this is a message', 'this is a detail', 'this is a hint', 'P0001'); +---- +NOTICE: this is a message +DETAIL: this is a detail +HINT: this is a hint +SQLSTATE: P0001 + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', 'division by zero msg', '', '', 'division_by_zero'); +---- +NOTICE: division by zero msg +SQLSTATE: 22012 + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', '', 'message is empty', '', 'P0001'); +---- +NOTICE: +DETAIL: message is empty +SQLSTATE: P0001 + +query T noticetrace +SELECT crdb_internal.plpgsql_raise('NOTICE', '', '', '', ''); +---- +NOTICE: +SQLSTATE: XXUUU + +query error pgcode 42704 pq: unrecognized exception condition: \"this_is_not_valid\" +SELECT crdb_internal.plpgsql_raise('NOTICE', '', '', '', 'this_is_not_valid'); + +query error pgcode 42704 pq: unrecognized exception condition: \"-50\" +SELECT crdb_internal.plpgsql_raise('NOTICE', '', '', '', '-50'); + +query error pgcode 22023 pq: severity NOTE is invalid +SELECT crdb_internal.plpgsql_raise('NOTE', '', '', '', '-50'); + +# Test severity ERROR. +query error pgcode XXUUU pq: foo +SELECT crdb_internal.plpgsql_raise('ERROR', 'foo', '', '', ''); + +query error pgcode 12345 pq: foo +SELECT crdb_internal.plpgsql_raise('ERROR', 'foo', '', '', '12345'); + +query error pgcode 12345 pq: msg\nHINT: hint\nDETAIL: detail +SELECT crdb_internal.plpgsql_raise('ERROR', 'msg', 'detail', 'hint', '12345'); + +query error pgcode XXUUU pq: +SELECT crdb_internal.plpgsql_raise('ERROR', '', '', '', ''); diff --git a/pkg/sql/logictest/testdata/logic_test/udf_plpgsql b/pkg/sql/logictest/testdata/logic_test/udf_plpgsql index e78987fba0e2..9bb544467d3f 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf_plpgsql +++ b/pkg/sql/logictest/testdata/logic_test/udf_plpgsql @@ -1,3 +1,7 @@ +statement ok +CREATE TABLE xy (x INT, y INT); +INSERT INTO xy VALUES (1, 2), (3, 4); + statement ok CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ BEGIN @@ -559,3 +563,271 @@ CREATE FUNCTION f_err(p1 RECORD) RETURNS RECORD AS $$ RETURN p1; END $$ LANGUAGE PLpgSQL; + +# Testing RAISE statements. +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE DEBUG 'foo'; + RAISE LOG 'foo'; + RAISE INFO 'foo'; + RAISE NOTICE 'foo'; + RAISE WARNING 'foo'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query T noticetrace +SELECT f(); +---- +INFO: foo +NOTICE: foo +WARNING: foo + +statement ok +SET client_min_messages = 'debug1'; + +query T noticetrace +SELECT f(); +---- +DEBUG1: foo +LOG: foo +INFO: foo +NOTICE: foo +WARNING: foo + +statement ok +RESET client_min_messages; + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE NOTICE '%', 1; + RAISE NOTICE 'foo: %, %, %', 1, 2, 3; + RAISE NOTICE '%%'; + RAISE NOTICE '%%%', 1; + RAISE NOTICE '%%%foo%% bar%%%% %% %%%% ba%z%', 1, 2, 3; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; + +query T noticetrace +SELECT f(); +---- +NOTICE: 1 +NOTICE: foo: 1, 2, 3 +NOTICE: % +NOTICE: %1 +NOTICE: %1foo% bar%% % %% ba2z3 + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE NOTICE division_by_zero; + RAISE NOTICE null_value_not_allowed; + RAISE NOTICE reading_sql_data_not_permitted; + RAISE NOTICE SQLSTATE '22012'; + RAISE NOTICE SQLSTATE '22004'; + RAISE NOTICE SQLSTATE '39004'; + RAISE NOTICE SQLSTATE '2F004'; + RAISE NOTICE SQLSTATE '38004'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query T noticetrace +SELECT f(); +---- +NOTICE: division_by_zero +SQLSTATE: 22012 +NOTICE: null_value_not_allowed +SQLSTATE: 22004 +NOTICE: reading_sql_data_not_permitted +SQLSTATE: 2F004 +NOTICE: 22012 +SQLSTATE: 22012 +NOTICE: 22004 +SQLSTATE: 22004 +NOTICE: 39004 +SQLSTATE: 39004 +NOTICE: 2F004 +SQLSTATE: 2F004 +NOTICE: 38004 +SQLSTATE: 38004 + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE NOTICE USING MESSAGE = 'foo'; + RAISE NOTICE USING MESSAGE = format('%s %s!','Hello','World'); + RAISE NOTICE USING MESSAGE = 'foo', DETAIL = 'bar', HINT = 'baz'; + RAISE NOTICE 'foo' USING ERRCODE = 'division_by_zero'; + RAISE NOTICE 'foo' USING ERRCODE = '22012'; + -- If no message is specified, the error code is used. + RAISE NOTICE USING ERRCODE = 'division_by_zero'; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; + +query T noticetrace +SELECT f(); +---- +NOTICE: foo +NOTICE: Hello World! +NOTICE: foo +DETAIL: bar +HINT: baz +NOTICE: foo +SQLSTATE: 22012 +NOTICE: foo +SQLSTATE: 22012 +NOTICE: division_by_zero +SQLSTATE: 22012 + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i INT := 0; + BEGIN + RAISE NOTICE '1: i = %', i; + i := 100; + RAISE NOTICE '2: i = %', i; + i := (SELECT count(*) FROM xy); + RAISE NOTICE '3: i = %', i; + RAISE NOTICE 'max_x: %', (SELECT max(x) FROM xy); + return i; + END +$$ LANGUAGE PLpgSQL; + +query T noticetrace +SELECT f(); +---- +NOTICE: 1: i = 0 +NOTICE: 2: i = 100 +NOTICE: 3: i = 2 +NOTICE: max_x: 3 + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i INT := 0; + BEGIN + LOOP + IF i >= 5 THEN EXIT; END IF; + RAISE NOTICE 'i = %', i; + i := i + 1; + END LOOP; + RAISE NOTICE 'finished with i = %', i; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; + +query T noticetrace +SELECT f(); +---- +NOTICE: i = 0 +NOTICE: i = 1 +NOTICE: i = 2 +NOTICE: i = 3 +NOTICE: i = 4 +NOTICE: finished with i = 5 + +# Testing RAISE statement with EXCEPTION log level. +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION 'foo'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query error pgcode P0001 pq: foo +SELECT f(); + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION division_by_zero; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query error pgcode 22012 pq: division_by_zero +SELECT f(); + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION SQLSTATE '22012'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query error pgcode 22012 pq: 22012 +SELECT f(); + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i INT := 0; + BEGIN + LOOP + IF i >= 5 THEN EXIT; END IF; + IF i = 3 THEN + RAISE EXCEPTION 'i = %', i; + END IF; + RAISE NOTICE 'i = %', i; + i := i + 1; + END LOOP; + RAISE NOTICE 'finished with i = %', i; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; + +query error pgcode P0001 pq: i = 3 +SELECT f(); + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION USING ERRCODE = 'division_by_zero'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query error pgcode 22012 pq: division_by_zero +SELECT f(); + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION USING ERRCODE = '22012'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query error pgcode 22012 pq: 22012 +SELECT f(); + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION USING DETAIL = 'use default errcode for the code and message'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query error pgcode P0001 pq: P0001\nDETAIL: use default errcode for the code and message +SELECT f(); + +# The default level is ERROR. +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE 'foo'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +query error pgcode P0001 pq: foo +SELECT f(); diff --git a/pkg/sql/logictest/tests/fakedist-disk/generated_test.go b/pkg/sql/logictest/tests/fakedist-disk/generated_test.go index 654d2275a0d5..2702962c07e6 100644 --- a/pkg/sql/logictest/tests/fakedist-disk/generated_test.go +++ b/pkg/sql/logictest/tests/fakedist-disk/generated_test.go @@ -1388,6 +1388,13 @@ func TestLogic_propagate_input_ordering( runLogicTest(t, "propagate_input_ordering") } +func TestLogic_raise( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "raise") +} + func TestLogic_reassign_owned_by( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/fakedist-vec-off/generated_test.go b/pkg/sql/logictest/tests/fakedist-vec-off/generated_test.go index f3f323571468..9b5b4d4b10a4 100644 --- a/pkg/sql/logictest/tests/fakedist-vec-off/generated_test.go +++ b/pkg/sql/logictest/tests/fakedist-vec-off/generated_test.go @@ -1388,6 +1388,13 @@ func TestLogic_propagate_input_ordering( runLogicTest(t, "propagate_input_ordering") } +func TestLogic_raise( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "raise") +} + func TestLogic_reassign_owned_by( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/fakedist/generated_test.go b/pkg/sql/logictest/tests/fakedist/generated_test.go index f2a1a86d1136..aec1ef0da99f 100644 --- a/pkg/sql/logictest/tests/fakedist/generated_test.go +++ b/pkg/sql/logictest/tests/fakedist/generated_test.go @@ -1402,6 +1402,13 @@ func TestLogic_propagate_input_ordering( runLogicTest(t, "propagate_input_ordering") } +func TestLogic_raise( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "raise") +} + func TestLogic_reassign_owned_by( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-legacy-schema-changer/generated_test.go b/pkg/sql/logictest/tests/local-legacy-schema-changer/generated_test.go index e88c79e2f40d..0451e951c829 100644 --- a/pkg/sql/logictest/tests/local-legacy-schema-changer/generated_test.go +++ b/pkg/sql/logictest/tests/local-legacy-schema-changer/generated_test.go @@ -1374,6 +1374,13 @@ func TestLogic_propagate_input_ordering( runLogicTest(t, "propagate_input_ordering") } +func TestLogic_raise( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "raise") +} + func TestLogic_reassign_owned_by( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-mixed-22.2-23.1/generated_test.go b/pkg/sql/logictest/tests/local-mixed-22.2-23.1/generated_test.go index 8b1253138f8a..88d9c0b23138 100644 --- a/pkg/sql/logictest/tests/local-mixed-22.2-23.1/generated_test.go +++ b/pkg/sql/logictest/tests/local-mixed-22.2-23.1/generated_test.go @@ -1374,6 +1374,13 @@ func TestLogic_propagate_input_ordering( runLogicTest(t, "propagate_input_ordering") } +func TestLogic_raise( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "raise") +} + func TestLogic_reassign_owned_by( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-vec-off/generated_test.go b/pkg/sql/logictest/tests/local-vec-off/generated_test.go index 2cc90bd09aca..50661f12d7a1 100644 --- a/pkg/sql/logictest/tests/local-vec-off/generated_test.go +++ b/pkg/sql/logictest/tests/local-vec-off/generated_test.go @@ -1402,6 +1402,13 @@ func TestLogic_propagate_input_ordering( runLogicTest(t, "propagate_input_ordering") } +func TestLogic_raise( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "raise") +} + func TestLogic_reassign_owned_by( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local/generated_test.go b/pkg/sql/logictest/tests/local/generated_test.go index 75f8bec21ddb..685c05074e50 100644 --- a/pkg/sql/logictest/tests/local/generated_test.go +++ b/pkg/sql/logictest/tests/local/generated_test.go @@ -1535,6 +1535,13 @@ func TestLogic_propagate_input_ordering( runLogicTest(t, "propagate_input_ordering") } +func TestLogic_raise( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "raise") +} + func TestLogic_rand_ident( t *testing.T, ) { diff --git a/pkg/sql/notice.go b/pkg/sql/notice.go index a3c31a64f0be..2eebde2c21da 100644 --- a/pkg/sql/notice.go +++ b/pkg/sql/notice.go @@ -31,7 +31,11 @@ var NoticesEnabled = settings.RegisterBoolSetting( // noticeSender is a subset of RestrictedCommandResult which allows // sending notices. type noticeSender interface { + // BufferNotice buffers the given notice to be flushed to the client before + // the connection is closed. BufferNotice(pgnotice.Notice) + // SendNotice immediately flushes the given notice to the client. + SendNotice(context.Context, pgnotice.Notice) error } // BufferClientNotice implements the eval.ClientNoticeSender interface. @@ -39,18 +43,37 @@ func (p *planner) BufferClientNotice(ctx context.Context, notice pgnotice.Notice if log.V(2) { log.Infof(ctx, "buffered notice: %+v", notice) } + if !p.checkNoticeSeverity(notice) { + return + } + p.noticeSender.BufferNotice(notice) +} + +// SendClientNotice implements the eval.ClientNoticeSender interface. +func (p *planner) SendClientNotice(ctx context.Context, notice pgnotice.Notice) error { + if log.V(2) { + log.Infof(ctx, "sending notice: %+v", notice) + } + if !p.checkNoticeSeverity(notice) { + return nil + } + return p.noticeSender.SendNotice(ctx, notice) +} + +func (p *planner) checkNoticeSeverity(notice pgnotice.Notice) bool { noticeSeverity, ok := pgnotice.ParseDisplaySeverity(pgerror.GetSeverity(notice)) if !ok { noticeSeverity = pgnotice.DisplaySeverityNotice } - if p.noticeSender == nil || - noticeSeverity > pgnotice.DisplaySeverity(p.SessionData().NoticeDisplaySeverity) || - !NoticesEnabled.Get(&p.execCfg.Settings.SV) { - // Notice cannot flow to the client - because of one of these conditions: - // * there is no client - // * the session's NoticeDisplaySeverity is higher than the severity of the notice. - // * the notice protocol was disabled - return - } - p.noticeSender.BufferNotice(notice) + // The notice can only flow to the client if the following are true: + // * there is a client + // * notice severity >= the session's NoticeDisplaySeverity + // * the notice protocol is enabled + // An exception to the second rule is DisplaySeverityInfo, which is always + // sent to the client if notices are enabled. + clientExists := p.noticeSender != nil + display := noticeSeverity <= pgnotice.DisplaySeverity(p.SessionData().NoticeDisplaySeverity) || + noticeSeverity == pgnotice.DisplaySeverityInfo + noticeEnabled := NoticesEnabled.Get(&p.execCfg.Settings.SV) + return clientExists && display && noticeEnabled } diff --git a/pkg/sql/opt/memo/expr.go b/pkg/sql/opt/memo/expr.go index 94fce5aa2e1a..9731810c5be8 100644 --- a/pkg/sql/opt/memo/expr.go +++ b/pkg/sql/opt/memo/expr.go @@ -706,6 +706,12 @@ type UDFDefinition struct { // applies to direct as well as indirect recursive calls (mutual recursion). IsRecursive bool + // Params is the list of columns representing parameters of the function. The + // i-th column in the list corresponds to the i-th parameter of the function. + // During execution of the UDF, these columns are replaced with the arguments + // of the function invocation. + Params opt.ColList + // Body contains a relational expression for each statement in the function // body. It is unset during construction of a recursive UDF. Body []RelExpr @@ -714,12 +720,6 @@ type UDFDefinition struct { // should be optimized if it is rebuilt. Each props corresponds to the RelExpr // at the same position in Body. BodyProps []*physical.Required - - // Params is the list of columns representing parameters of the function. The - // i-th column in the list corresponds to the i-th parameter of the function. - // During execution of the UDF, these columns are replaced with the arguments - // of the function invocation. - Params opt.ColList } // WindowFrame denotes the definition of a window frame for an individual diff --git a/pkg/sql/opt/optbuilder/plpgsql.go b/pkg/sql/opt/optbuilder/plpgsql.go index 3bf41b1345d2..e42efb6af0d9 100644 --- a/pkg/sql/opt/optbuilder/plpgsql.go +++ b/pkg/sql/opt/optbuilder/plpgsql.go @@ -12,12 +12,14 @@ package optbuilder import ( "fmt" + "strings" "github.com/cockroachdb/cockroach/pkg/sql/opt" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" "github.com/cockroachdb/cockroach/pkg/sql/opt/props/physical" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/cockroach/pkg/sql/sem/builtins/builtinsregistry" "github.com/cockroachdb/cockroach/pkg/sql/sem/plpgsqltree" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/sem/volatility" @@ -345,6 +347,40 @@ func (b *plpgsqlBuilder) buildPLpgSQLStatements( } else { panic(pgerror.New(pgcode.Syntax, "CONTINUE cannot be used outside a loop")) } + case *plpgsqltree.PLpgSQLStmtRaise: + // RAISE statements allow the PLpgSQL function to send an error or a + // notice to the client. We handle these side effects by building them + // into a separate body statement that is only executed for its side + // effects. The remaining PLpgSQL statements then become the last body + // statement, which returns the actual result of evaluation. + // + // The synchronous notice sending behavior is implemented in the + // crdb_internal.plpgsql_raise builtin function. The side-effecting body + // statement just makes a call into crdb_internal.plpgsql_raise using the + // RAISE statement options as parameters. + con := b.makeContinuation("_stmt_raise") + const raiseFnName = "crdb_internal.plpgsql_raise" + props, overloads := builtinsregistry.GetBuiltinProperties(raiseFnName) + if len(overloads) != 1 { + panic(errors.AssertionFailedf("expected one overload for %s", raiseFnName)) + } + raiseCall := b.ob.factory.ConstructFunction( + b.getRaiseArgs(con.s, t), + &memo.FunctionPrivate{ + Name: raiseFnName, + Typ: types.Int, + Properties: props, + Overload: &overloads[0], + }, + ) + raiseColName := scopeColName("").WithMetadataName(b.makeIdentifier("stmt_raise")) + raiseScope := con.s.push() + b.ob.synthesizeColumn(raiseScope, raiseColName, types.Int, nil /* expr */, raiseCall) + b.ob.constructProjectForScope(con.s, raiseScope) + con.def.Body = []memo.RelExpr{raiseScope.expr} + con.def.BodyProps = []*physical.Required{raiseScope.makePhysicalProps()} + b.finishContinuation(stmts[i+1:], &con, false /* recursive */) + return b.callContinuation(&con, s) default: panic(unimplemented.New( "unimplemented PL/pgSQL statement", @@ -386,25 +422,138 @@ func (b *plpgsqlBuilder) addPLpgSQLAssign( return assignScope } -// makeContinuation allocates a new continuation function with an uninitialized -// definition. -func (b *plpgsqlBuilder) makeContinuation(name string) continuation { - return continuation{ - def: &memo.UDFDefinition{ - Name: b.makeIdentifier(name), - Typ: b.returnType, - CalledOnNullInput: true, - }, +// getRaiseArgs validates the options attached to the given PLpgSQL RAISE +// statement and returns the arguments to be used for a call to the +// crdb_internal.plpgsql_raise builtin function. +func (b *plpgsqlBuilder) getRaiseArgs( + s *scope, raise *plpgsqltree.PLpgSQLStmtRaise, +) memo.ScalarListExpr { + var severity, message, detail, hint, code opt.ScalarExpr + makeConstStr := func(str string) opt.ScalarExpr { + return b.ob.factory.ConstructConstVal(tree.NewDString(str), types.String) + } + // Retrieve the error/notice severity. + logLevel := strings.ToUpper(raise.LogLevel) + if logLevel == "" { + // EXCEPTION is the default log level. + logLevel = "EXCEPTION" + } + switch logLevel { + case "EXCEPTION": + // ERROR is the equivalent severity to log-level EXCEPTION. + severity = makeConstStr("ERROR") + case "LOG", "INFO", "NOTICE", "WARNING": + severity = makeConstStr(logLevel) + case "DEBUG": + // DEBUG log-level maps to severity DEBUG1. + severity = makeConstStr("DEBUG1") + default: + panic(unimplemented.Newf( + "unimplemented log level", "RAISE log level %s is not yet supported", raise.LogLevel, + )) + } + // Retrieve the message, if it was set with the format syntax. + if raise.Message != "" { + message = b.makeRaiseFormatMessage(s, raise.Message, raise.Params) + } + if raise.Code != "" { + code = makeConstStr(raise.Code) + } else if raise.CodeName != "" { + code = makeConstStr(raise.CodeName) } + // Retrieve the RAISE options, if any. + buildOptionExpr := func(name string, expr plpgsqltree.PLpgSQLExpr, isDup bool) opt.ScalarExpr { + if isDup { + panic(pgerror.Newf(pgcode.Syntax, "RAISE option already specified: %s", name)) + } + return b.buildPLpgSQLExpr(expr, types.String, s) + } + for _, option := range raise.Options { + optName := strings.ToUpper(option.OptType) + switch optName { + case "MESSAGE": + message = buildOptionExpr(optName, option.Expr, message != nil) + case "DETAIL": + detail = buildOptionExpr(optName, option.Expr, detail != nil) + case "HINT": + hint = buildOptionExpr(optName, option.Expr, hint != nil) + case "ERRCODE": + code = buildOptionExpr(optName, option.Expr, code != nil) + case "COLUMN", "CONSTRAINT", "DATATYPE", "TABLE", "SCHEMA": + panic(unimplemented.NewWithIssuef(106237, "RAISE option %s is not yet implemented", optName)) + default: + panic(errors.AssertionFailedf("unrecognized RAISE option: %s", option.OptType)) + } + } + if code == nil { + if logLevel == "EXCEPTION" { + // The default error code for EXCEPTION is ERRCODE_RAISE_EXCEPTION. + code = makeConstStr(pgcode.RaiseException.String()) + } else { + code = makeConstStr(pgcode.SuccessfulCompletion.String()) + } + } + // If no message text is supplied, use the error code or condition name. + if message == nil { + message = code + } + args := memo.ScalarListExpr{severity, message, detail, hint, code} + for i := range args { + if args[i] == nil { + args[i] = makeConstStr("") + } + } + return args } -// finishContinuation initializes the definition of a continuation function with -// the function body. It is separate from makeContinuation to allow recursive -// function definitions, which need to push the continuation before it is -// finished. -func (b *plpgsqlBuilder) finishContinuation( - stmts []plpgsqltree.PLpgSQLStatement, con *continuation, recursive bool, -) { +// A PLpgSQL RAISE statement can specify a format string, where supplied +// expressions replace instances of '%' in the string. A literal '%' character +// is specified by doubling it: '%%'. The formatting arguments can be arbitrary +// SQL expressions. +func (b *plpgsqlBuilder) makeRaiseFormatMessage( + s *scope, format string, args []plpgsqltree.PLpgSQLExpr, +) (result opt.ScalarExpr) { + makeConstStr := func(str string) opt.ScalarExpr { + return b.ob.factory.ConstructConstVal(tree.NewDString(str), types.String) + } + addToResult := func(expr opt.ScalarExpr) { + if result == nil { + result = expr + } else { + // Concatenate the previously built string with the current one. + result = b.ob.factory.ConstructConcat(result, expr) + } + } + // Split the format string on each pair of '%' characters; any '%' characters + // in the substrings are formatting parameters. + var argIdx int + for i, literalSubstr := range strings.Split(format, "%%") { + if i > 0 { + // Add the literal '%' character in place of the matched '%%'. + addToResult(makeConstStr("%")) + } + // Split on the parameter characters '%'. + for j, paramSubstr := range strings.Split(literalSubstr, "%") { + if j > 0 { + // Add the next argument at the location of this parameter. + if argIdx >= len(args) { + panic(pgerror.Newf(pgcode.PLpgSQL, "too few parameters specified for RAISE")) + } + addToResult(b.buildPLpgSQLExpr(args[argIdx], types.String, s)) + argIdx++ + } + addToResult(makeConstStr(paramSubstr)) + } + } + if argIdx < len(args) { + panic(pgerror.Newf(pgcode.PLpgSQL, "too many parameters specified for RAISE")) + } + return result +} + +// makeContinuation allocates a new continuation function with an uninitialized +// definition. +func (b *plpgsqlBuilder) makeContinuation(name string) continuation { s := b.ob.allocScope() b.ensureScopeHasExpr(s) params := make(opt.ColList, 0, len(b.decls)+len(b.params)) @@ -422,18 +571,42 @@ func (b *plpgsqlBuilder) finishContinuation( for _, param := range b.params { addParam(tree.Name(param.Name), param.Typ) } + return continuation{ + def: &memo.UDFDefinition{ + Params: params, + Name: b.makeIdentifier(name), + Typ: b.returnType, + CalledOnNullInput: true, + }, + s: s, + } +} + +// finishContinuation adds the final body statement to the definition of a +// continuation function. This statement returns the result of executing the +// given PLpgSQL statements. There may be other statements that are executed +// before this final statement for their side effects (e.g. RAISE statement). +// +// finishContinuation is separate from makeContinuation to allow recursive +// function definitions, which need to push the continuation before it is +// finished. +func (b *plpgsqlBuilder) finishContinuation( + stmts []plpgsqltree.PLpgSQLStatement, con *continuation, recursive bool, +) { // Make sure to push s before constructing the continuation scope to ensure // that the parameter columns are not projected. - continuationScope := b.buildPLpgSQLStatements(stmts, s.push()) + continuationScope := b.buildPLpgSQLStatements(stmts, con.s.push()) if continuationScope == nil { // One or more branches did not terminate with a RETURN statement. con.reachedEndOfFunction = true return } + // Append to the body statements because some PLpgSQL statements will make a + // continuation routine with more than one body statement in order to handle + // side effects (see the RAISE case in buildPLpgSQLStatements). + con.def.Body = append(con.def.Body, continuationScope.expr) + con.def.BodyProps = append(con.def.BodyProps, continuationScope.makePhysicalProps()) con.def.IsRecursive = recursive - con.def.Body = []memo.RelExpr{continuationScope.expr} - con.def.BodyProps = []*physical.Required{continuationScope.makePhysicalProps()} - con.def.Params = params // Set the volatility of the continuation routine to the least restrictive // volatility level in the expression's Relational properties. vol := continuationScope.expr.Relational().VolatilitySet @@ -514,6 +687,10 @@ type continuation struct { // from a branch in the control flow. def *memo.UDFDefinition + // s is a scope initialized with the parameters of the routine. It should be + // used to construct the routine body statement. + s *scope + // isLoopContinuation indicates that this continuation was constructed for the // body statements of a loop. isLoopContinuation bool diff --git a/pkg/sql/opt/optbuilder/testdata/udf_plpgsql b/pkg/sql/opt/optbuilder/testdata/udf_plpgsql index 0aa9bb808e01..c86e57693c3a 100644 --- a/pkg/sql/opt/optbuilder/testdata/udf_plpgsql +++ b/pkg/sql/opt/optbuilder/testdata/udf_plpgsql @@ -1,3 +1,7 @@ +exec-ddl +CREATE TABLE xy (x INT, y INT); +---- + exec-ddl CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ BEGIN @@ -1538,3 +1542,1540 @@ project │ │ └── variable: b:39 │ └── recursive-call └── const: 1 + +# TODO(drewk): consider adding a norm rules to fold nested UDFs. +# Testing RAISE statements. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE DEBUG 'foo'; + RAISE LOG 'foo'; + RAISE INFO 'foo'; + RAISE NOTICE 'foo'; + RAISE WARNING 'foo'; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:12 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:12] + └── body + └── limit + ├── columns: "_stmt_raise_1":11 + ├── project + │ ├── columns: "_stmt_raise_1":11 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":11] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'DEBUG1' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_3":10 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":10] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_4:2 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_4:2] + │ │ ├── const: 'LOG' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_5":9 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":9] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_6:3 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_6:3] + │ │ ├── const: 'INFO' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_7":8 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":8] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_8:4 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_8:4] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_9":7 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":7] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_10:5 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_10:5] + │ │ ├── const: 'WARNING' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: stmt_return_11:6!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_11:6] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE NOTICE '%', 1; + RAISE NOTICE 'foo: %, %, %', 1, 2, 3; + RAISE NOTICE '%%'; + RAISE NOTICE '%%%', 1; + RAISE NOTICE '%%%foo%% bar%%%% %% %%%% ba%z%', 1, 2, 3; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:12 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:12] + └── body + └── limit + ├── columns: "_stmt_raise_1":11 + ├── project + │ ├── columns: "_stmt_raise_1":11 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":11] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── const: '' + │ │ │ │ └── const: 1 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_3":10 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":10] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_4:2 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_4:2] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── concat + │ │ │ │ │ ├── concat + │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ ├── const: 'foo: ' + │ │ │ │ │ │ │ │ └── const: 1 + │ │ │ │ │ │ │ └── const: ', ' + │ │ │ │ │ │ └── const: 2 + │ │ │ │ │ └── const: ', ' + │ │ │ │ └── const: 3 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_5":9 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":9] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_6:3 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_6:3] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── const: '' + │ │ │ │ └── const: '%' + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_7":8 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":8] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_8:4 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_8:4] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── concat + │ │ │ │ │ ├── concat + │ │ │ │ │ │ ├── const: '' + │ │ │ │ │ │ └── const: '%' + │ │ │ │ │ └── const: '' + │ │ │ │ └── const: 1 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_9":7 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":7] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_10:5 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_10:5] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── concat + │ │ │ │ │ ├── concat + │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── concat + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── const: '' + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: '%' + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: '' + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: 1 + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: 'foo' + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: '%' + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: ' bar' + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: '%' + │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: '' + │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: '%' + │ │ │ │ │ │ │ │ │ │ │ │ │ └── const: ' ' + │ │ │ │ │ │ │ │ │ │ │ │ └── const: '%' + │ │ │ │ │ │ │ │ │ │ │ └── const: ' ' + │ │ │ │ │ │ │ │ │ │ └── const: '%' + │ │ │ │ │ │ │ │ │ └── const: '' + │ │ │ │ │ │ │ │ └── const: '%' + │ │ │ │ │ │ │ └── const: ' ba' + │ │ │ │ │ │ └── const: 2 + │ │ │ │ │ └── const: 'z' + │ │ │ │ └── const: 3 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: stmt_return_11:6!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_11:6] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE NOTICE division_by_zero; + RAISE NOTICE null_value_not_allowed; + RAISE NOTICE reading_sql_data_not_permitted; + RAISE NOTICE SQLSTATE '22012'; + RAISE NOTICE SQLSTATE '22004'; + RAISE NOTICE SQLSTATE '39004'; + RAISE NOTICE SQLSTATE '2F004'; + RAISE NOTICE SQLSTATE '38004'; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:18 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:18] + └── body + └── limit + ├── columns: "_stmt_raise_1":17 + ├── project + │ ├── columns: "_stmt_raise_1":17 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":17] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'division_by_zero' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'division_by_zero' + │ └── project + │ ├── columns: "_stmt_raise_3":16 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":16] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_4:2 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_4:2] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'null_value_not_allowed' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'null_value_not_allowed' + │ └── project + │ ├── columns: "_stmt_raise_5":15 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":15] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_6:3 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_6:3] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'reading_sql_data_not_permitted' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'reading_sql_data_not_permitted' + │ └── project + │ ├── columns: "_stmt_raise_7":14 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":14] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_8:4 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_8:4] + │ │ ├── const: 'NOTICE' + │ │ ├── const: '22012' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '22012' + │ └── project + │ ├── columns: "_stmt_raise_9":13 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":13] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_10:5 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_10:5] + │ │ ├── const: 'NOTICE' + │ │ ├── const: '22004' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '22004' + │ └── project + │ ├── columns: "_stmt_raise_11":12 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_11 [as="_stmt_raise_11":12] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_12:6 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_12:6] + │ │ ├── const: 'NOTICE' + │ │ ├── const: '39004' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '39004' + │ └── project + │ ├── columns: "_stmt_raise_13":11 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_13 [as="_stmt_raise_13":11] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_14:7 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_14:7] + │ │ ├── const: 'NOTICE' + │ │ ├── const: '2F004' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '2F004' + │ └── project + │ ├── columns: "_stmt_raise_15":10 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_15 [as="_stmt_raise_15":10] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_16:8 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_16:8] + │ │ ├── const: 'NOTICE' + │ │ ├── const: '38004' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '38004' + │ └── project + │ ├── columns: stmt_return_17:9!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_17:9] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE NOTICE USING MESSAGE = 'foo'; + RAISE NOTICE USING MESSAGE = format('%s %s!','Hello','World'); + RAISE NOTICE USING MESSAGE = 'foo', DETAIL = 'bar', HINT = 'baz'; + RAISE NOTICE 'foo' USING ERRCODE = 'division_by_zero'; + RAISE NOTICE 'foo' USING ERRCODE = '22012'; + -- If no message is specified, the error code is used. + RAISE NOTICE USING ERRCODE = 'division_by_zero'; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:14 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:14] + └── body + └── limit + ├── columns: "_stmt_raise_1":13 + ├── project + │ ├── columns: "_stmt_raise_1":13 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":13] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_3":12 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":12] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_4:2 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_4:2] + │ │ ├── const: 'NOTICE' + │ │ ├── function: format + │ │ │ ├── const: '%s %s!' + │ │ │ ├── const: 'Hello' + │ │ │ └── const: 'World' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_5":11 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":11] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_6:3 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_6:3] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'foo' + │ │ ├── const: 'bar' + │ │ ├── const: 'baz' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_7":10 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":10] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_8:4 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_8:4] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'division_by_zero' + │ └── project + │ ├── columns: "_stmt_raise_9":9 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":9] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_10:5 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_10:5] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '22012' + │ └── project + │ ├── columns: "_stmt_raise_11":8 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_11 [as="_stmt_raise_11":8] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_12:6 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_12:6] + │ │ ├── const: 'NOTICE' + │ │ ├── const: 'division_by_zero' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'division_by_zero' + │ └── project + │ ├── columns: stmt_return_13:7!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_13:7] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i INT := 0; + BEGIN + RAISE NOTICE '1: i = %', i; + i := 100; + RAISE NOTICE '2: i = %', i; + i := (SELECT count(*) FROM xy); + RAISE NOTICE '3: i = %', i; + RAISE NOTICE 'max_x: %', (SELECT max(x) FROM xy); + return i; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:29 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:29] + └── body + └── limit + ├── columns: "_stmt_raise_1":28 + ├── project + │ ├── columns: "_stmt_raise_1":28 + │ ├── project + │ │ ├── columns: i:1!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 0 [as=i:1] + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":28] + │ ├── args + │ │ └── variable: i:1 + │ ├── params: i:2 + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:3 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:3] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── const: '1: i = ' + │ │ │ │ └── variable: i:2 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_3":27 + │ ├── project + │ │ ├── columns: i:4!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 100 [as=i:4] + │ └── projections + │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":27] + │ ├── args + │ │ └── variable: i:4 + │ ├── params: i:5 + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_4:6 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_4:6] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── const: '2: i = ' + │ │ │ │ └── variable: i:5 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_5":26 + │ ├── project + │ │ ├── columns: i:13 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── subquery [as=i:13] + │ │ └── max1-row + │ │ ├── columns: count_rows:12!null + │ │ └── scalar-group-by + │ │ ├── columns: count_rows:12!null + │ │ ├── project + │ │ │ └── scan xy + │ │ │ └── columns: x:7 y:8 rowid:9!null crdb_internal_mvcc_timestamp:10 tableoid:11 + │ │ └── aggregations + │ │ └── count-rows [as=count_rows:12] + │ └── projections + │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":26] + │ ├── args + │ │ └── variable: i:13 + │ ├── params: i:14 + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_6:15 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_6:15] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── const: '3: i = ' + │ │ │ │ └── variable: i:14 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: "_stmt_raise_7":25 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":25] + │ ├── args + │ │ └── variable: i:14 + │ ├── params: i:16 + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_8:23 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_8:23] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── const: 'max_x: ' + │ │ │ │ └── subquery + │ │ │ │ └── max1-row + │ │ │ │ ├── columns: max:22 + │ │ │ │ └── scalar-group-by + │ │ │ │ ├── columns: max:22 + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: x:17 + │ │ │ │ │ └── scan xy + │ │ │ │ │ └── columns: x:17 y:18 rowid:19!null crdb_internal_mvcc_timestamp:20 tableoid:21 + │ │ │ │ └── aggregations + │ │ │ │ └── max [as=max:22] + │ │ │ │ └── variable: x:17 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: stmt_return_9:24 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── variable: i:16 [as=stmt_return_9:24] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i INT := 0; + BEGIN + LOOP + IF i >= 5 THEN EXIT; END IF; + RAISE NOTICE 'i = %', i; + i := i + 1; + END LOOP; + RAISE NOTICE 'finished with i = %', i; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:18 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:18] + └── body + └── limit + ├── columns: stmt_loop_5:17 + ├── project + │ ├── columns: stmt_loop_5:17 + │ ├── project + │ │ ├── columns: i:1!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 0 [as=i:1] + │ └── projections + │ └── udf: stmt_loop_5 [as=stmt_loop_5:17] + │ ├── args + │ │ └── variable: i:1 + │ ├── params: i:7 + │ └── body + │ └── project + │ ├── columns: stmt_if_9:16 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── case [as=stmt_if_9:16] + │ ├── true + │ ├── when + │ │ ├── ge + │ │ │ ├── variable: i:7 + │ │ │ └── const: 5 + │ │ └── subquery + │ │ └── project + │ │ ├── columns: loop_exit_1:14 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── udf: loop_exit_1 [as=loop_exit_1:14] + │ │ ├── args + │ │ │ └── variable: i:7 + │ │ ├── params: i:2 + │ │ └── body + │ │ └── project + │ │ ├── columns: "_stmt_raise_2":6 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── udf: _stmt_raise_2 [as="_stmt_raise_2":6] + │ │ ├── args + │ │ │ └── variable: i:2 + │ │ ├── params: i:3 + │ │ └── body + │ │ ├── project + │ │ │ ├── columns: stmt_raise_3:4 + │ │ │ ├── values + │ │ │ │ └── tuple + │ │ │ └── projections + │ │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_3:4] + │ │ │ ├── const: 'NOTICE' + │ │ │ ├── concat + │ │ │ │ ├── concat + │ │ │ │ │ ├── const: 'finished with i = ' + │ │ │ │ │ └── variable: i:3 + │ │ │ │ └── const: '' + │ │ │ ├── const: '' + │ │ │ ├── const: '' + │ │ │ └── const: '00000' + │ │ └── project + │ │ ├── columns: stmt_return_4:5!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 0 [as=stmt_return_4:5] + │ └── subquery + │ └── project + │ ├── columns: stmt_if_6:15 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: stmt_if_6 [as=stmt_if_6:15] + │ ├── args + │ │ └── variable: i:7 + │ ├── params: i:8 + │ └── body + │ └── project + │ ├── columns: "_stmt_raise_7":13 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":13] + │ ├── args + │ │ └── variable: i:8 + │ ├── params: i:9 + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_8:10 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_8:10] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── const: 'i = ' + │ │ │ │ └── variable: i:9 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: stmt_loop_5:12 + │ ├── project + │ │ ├── columns: i:11 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── plus [as=i:11] + │ │ ├── variable: i:9 + │ │ └── const: 1 + │ └── projections + │ └── udf: stmt_loop_5 [as=stmt_loop_5:12] + │ ├── args + │ │ └── variable: i:11 + │ └── recursive-call + └── const: 1 + +# Testing RAISE statement with EXCEPTION log level. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION 'foo'; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:4 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:4] + └── body + └── limit + ├── columns: "_stmt_raise_1":3 + ├── project + │ ├── columns: "_stmt_raise_1":3 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":3] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'ERROR' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'P0001' + │ └── project + │ ├── columns: stmt_return_3:2!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_3:2] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION division_by_zero; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:4 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:4] + └── body + └── limit + ├── columns: "_stmt_raise_1":3 + ├── project + │ ├── columns: "_stmt_raise_1":3 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":3] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'ERROR' + │ │ ├── const: 'division_by_zero' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'division_by_zero' + │ └── project + │ ├── columns: stmt_return_3:2!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_3:2] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION SQLSTATE '22012'; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:4 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:4] + └── body + └── limit + ├── columns: "_stmt_raise_1":3 + ├── project + │ ├── columns: "_stmt_raise_1":3 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":3] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'ERROR' + │ │ ├── const: '22012' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '22012' + │ └── project + │ ├── columns: stmt_return_3:2!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_3:2] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i INT := 0; + BEGIN + LOOP + IF i >= 5 THEN EXIT; END IF; + IF i = 3 THEN + RAISE EXCEPTION 'i = %', i; + END IF; + RAISE NOTICE 'i = %', i; + i := i + 1; + END LOOP; + RAISE NOTICE 'finished with i = %', i; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:25 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:25] + └── body + └── limit + ├── columns: stmt_loop_5:24 + ├── project + │ ├── columns: stmt_loop_5:24 + │ ├── project + │ │ ├── columns: i:1!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 0 [as=i:1] + │ └── projections + │ └── udf: stmt_loop_5 [as=stmt_loop_5:24] + │ ├── args + │ │ └── variable: i:1 + │ ├── params: i:7 + │ └── body + │ └── project + │ ├── columns: stmt_if_13:23 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── case [as=stmt_if_13:23] + │ ├── true + │ ├── when + │ │ ├── ge + │ │ │ ├── variable: i:7 + │ │ │ └── const: 5 + │ │ └── subquery + │ │ └── project + │ │ ├── columns: loop_exit_1:21 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── udf: loop_exit_1 [as=loop_exit_1:21] + │ │ ├── args + │ │ │ └── variable: i:7 + │ │ ├── params: i:2 + │ │ └── body + │ │ └── project + │ │ ├── columns: "_stmt_raise_2":6 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── udf: _stmt_raise_2 [as="_stmt_raise_2":6] + │ │ ├── args + │ │ │ └── variable: i:2 + │ │ ├── params: i:3 + │ │ └── body + │ │ ├── project + │ │ │ ├── columns: stmt_raise_3:4 + │ │ │ ├── values + │ │ │ │ └── tuple + │ │ │ └── projections + │ │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_3:4] + │ │ │ ├── const: 'NOTICE' + │ │ │ ├── concat + │ │ │ │ ├── concat + │ │ │ │ │ ├── const: 'finished with i = ' + │ │ │ │ │ └── variable: i:3 + │ │ │ │ └── const: '' + │ │ │ ├── const: '' + │ │ │ ├── const: '' + │ │ │ └── const: '00000' + │ │ └── project + │ │ ├── columns: stmt_return_4:5!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 0 [as=stmt_return_4:5] + │ └── subquery + │ └── project + │ ├── columns: stmt_if_6:22 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: stmt_if_6 [as=stmt_if_6:22] + │ ├── args + │ │ └── variable: i:7 + │ ├── params: i:8 + │ └── body + │ └── project + │ ├── columns: stmt_if_12:20 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── case [as=stmt_if_12:20] + │ ├── true + │ ├── when + │ │ ├── eq + │ │ │ ├── variable: i:8 + │ │ │ └── const: 3 + │ │ └── subquery + │ │ └── project + │ │ ├── columns: "_stmt_raise_10":18 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── udf: _stmt_raise_10 [as="_stmt_raise_10":18] + │ │ ├── args + │ │ │ └── variable: i:8 + │ │ ├── params: i:15 + │ │ └── body + │ │ ├── project + │ │ │ ├── columns: stmt_raise_11:16 + │ │ │ ├── values + │ │ │ │ └── tuple + │ │ │ └── projections + │ │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_11:16] + │ │ │ ├── const: 'ERROR' + │ │ │ ├── concat + │ │ │ │ ├── concat + │ │ │ │ │ ├── const: 'i = ' + │ │ │ │ │ └── variable: i:15 + │ │ │ │ └── const: '' + │ │ │ ├── const: '' + │ │ │ ├── const: '' + │ │ │ └── const: 'P0001' + │ │ └── project + │ │ ├── columns: stmt_if_7:17 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── udf: stmt_if_7 [as=stmt_if_7:17] + │ │ ├── args + │ │ │ └── variable: i:15 + │ │ ├── params: i:9 + │ │ └── body + │ │ └── project + │ │ ├── columns: "_stmt_raise_8":14 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── udf: _stmt_raise_8 [as="_stmt_raise_8":14] + │ │ ├── args + │ │ │ └── variable: i:9 + │ │ ├── params: i:10 + │ │ └── body + │ │ ├── project + │ │ │ ├── columns: stmt_raise_9:11 + │ │ │ ├── values + │ │ │ │ └── tuple + │ │ │ └── projections + │ │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_9:11] + │ │ │ ├── const: 'NOTICE' + │ │ │ ├── concat + │ │ │ │ ├── concat + │ │ │ │ │ ├── const: 'i = ' + │ │ │ │ │ └── variable: i:10 + │ │ │ │ └── const: '' + │ │ │ ├── const: '' + │ │ │ ├── const: '' + │ │ │ └── const: '00000' + │ │ └── project + │ │ ├── columns: stmt_loop_5:13 + │ │ ├── project + │ │ │ ├── columns: i:12 + │ │ │ ├── values + │ │ │ │ └── tuple + │ │ │ └── projections + │ │ │ └── plus [as=i:12] + │ │ │ ├── variable: i:10 + │ │ │ └── const: 1 + │ │ └── projections + │ │ └── udf: stmt_loop_5 [as=stmt_loop_5:13] + │ │ ├── args + │ │ │ └── variable: i:12 + │ │ └── recursive-call + │ └── subquery + │ └── project + │ ├── columns: stmt_if_7:19 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: stmt_if_7 [as=stmt_if_7:19] + │ ├── args + │ │ └── variable: i:8 + │ ├── params: i:9 + │ └── body + │ └── project + │ ├── columns: "_stmt_raise_8":14 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_8 [as="_stmt_raise_8":14] + │ ├── args + │ │ └── variable: i:9 + │ ├── params: i:10 + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_9:11 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_9:11] + │ │ ├── const: 'NOTICE' + │ │ ├── concat + │ │ │ ├── concat + │ │ │ │ ├── const: 'i = ' + │ │ │ │ └── variable: i:10 + │ │ │ └── const: '' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '00000' + │ └── project + │ ├── columns: stmt_loop_5:13 + │ ├── project + │ │ ├── columns: i:12 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── plus [as=i:12] + │ │ ├── variable: i:10 + │ │ └── const: 1 + │ └── projections + │ └── udf: stmt_loop_5 [as=stmt_loop_5:13] + │ ├── args + │ │ └── variable: i:12 + │ └── recursive-call + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION USING ERRCODE = 'division_by_zero'; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:4 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:4] + └── body + └── limit + ├── columns: "_stmt_raise_1":3 + ├── project + │ ├── columns: "_stmt_raise_1":3 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":3] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'ERROR' + │ │ ├── const: 'division_by_zero' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'division_by_zero' + │ └── project + │ ├── columns: stmt_return_3:2!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_3:2] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION USING ERRCODE = '22012'; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:4 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:4] + └── body + └── limit + ├── columns: "_stmt_raise_1":3 + ├── project + │ ├── columns: "_stmt_raise_1":3 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":3] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'ERROR' + │ │ ├── const: '22012' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: '22012' + │ └── project + │ ├── columns: stmt_return_3:2!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_3:2] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION USING DETAIL = 'use default errcode for the code and message'; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:4 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:4] + └── body + └── limit + ├── columns: "_stmt_raise_1":3 + ├── project + │ ├── columns: "_stmt_raise_1":3 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":3] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'ERROR' + │ │ ├── const: 'P0001' + │ │ ├── const: 'use default errcode for the code and message' + │ │ ├── const: '' + │ │ └── const: 'P0001' + │ └── project + │ ├── columns: stmt_return_3:2!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_3:2] + └── const: 1 + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE 'foo'; + return 0; + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT f(); +---- +project + ├── columns: f:4 + ├── values + │ └── tuple + └── projections + └── udf: f [as=f:4] + └── body + └── limit + ├── columns: "_stmt_raise_1":3 + ├── project + │ ├── columns: "_stmt_raise_1":3 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":3] + │ └── body + │ ├── project + │ │ ├── columns: stmt_raise_2:1 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:1] + │ │ ├── const: 'ERROR' + │ │ ├── const: 'foo' + │ │ ├── const: '' + │ │ ├── const: '' + │ │ └── const: 'P0001' + │ └── project + │ ├── columns: stmt_return_3:2!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_3:2] + └── const: 1 diff --git a/pkg/sql/pgwire/command_result.go b/pkg/sql/pgwire/command_result.go index 462a728d88a2..f401f9635b5b 100644 --- a/pkg/sql/pgwire/command_result.go +++ b/pkg/sql/pgwire/command_result.go @@ -288,6 +288,14 @@ func (r *commandResult) BufferNotice(notice pgnotice.Notice) { r.buffer.notices = append(r.buffer.notices, notice) } +// SendNotice is part of the sql.RestrictedCommandResult interface. +func (r *commandResult) SendNotice(ctx context.Context, notice pgnotice.Notice) error { + if err := r.conn.bufferNotice(ctx, notice); err != nil { + return err + } + return r.conn.Flush(r.pos) +} + // SetColumns is part of the sql.RestrictedCommandResult interface. func (r *commandResult) SetColumns(ctx context.Context, cols colinfo.ResultColumns) { r.assertNotReleased() diff --git a/pkg/sql/pgwire/pgcode/BUILD.bazel b/pkg/sql/pgwire/pgcode/BUILD.bazel index c778edc2b198..625b12ef5a0e 100644 --- a/pkg/sql/pgwire/pgcode/BUILD.bazel +++ b/pkg/sql/pgwire/pgcode/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "codes.go", "doc.go", + "plpgsql_codenames.go", ], importpath = "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode", visibility = ["//visibility:public"], diff --git a/pkg/sql/pgwire/pgcode/generate_names.sh b/pkg/sql/pgwire/pgcode/generate_names.sh new file mode 100644 index 000000000000..1d4b316941b4 --- /dev/null +++ b/pkg/sql/pgwire/pgcode/generate_names.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -eu + +# This script will generate a mapping from condition names to error codes. +# It will not perform any cleanup, so some manual post-processing may be +# necessary for duplicate 'Error' strings, capitalizing initialisms and +# acronyms, and fixing Golint errors. +sed '/^\s*$/d' errcodes.txt | +sed '/^#.*$/d' | +sed -E 's|^(Section.*)$|// \1|' | +sed -E 's|^([A-Z0-9]{5}) . ([A-Z_]+)[[:space:]]+([a-z_]+).*$|"\3": {"\1"},|' | +# Postgres uses class 58 just for external errors, but we've extended it with some errors +# internal to the cluster (inspired by DB2). +sed -E 's|// Section: Class 58 - System Error \(errors external to PostgreSQL itself\)|// Section: Class 58 - System Error|' > errcodes.generated diff --git a/pkg/sql/pgwire/pgcode/plpgsql_codenames.go b/pkg/sql/pgwire/pgcode/plpgsql_codenames.go new file mode 100644 index 000000000000..bc3958eaaba7 --- /dev/null +++ b/pkg/sql/pgwire/pgcode/plpgsql_codenames.go @@ -0,0 +1,294 @@ +// Copyright 2023 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 pgcode + +// PLpgSQLConditionNameToCode maps from PG error condition names to error codes. +// Most of the condition names map to one code, but a few (moved to the bottom) +// map to two codes. This is used for PLpgSQL exception handling. +// +// PLpgSQLConditionNameToCode was generated using the generate_names.sh script, +// then manually edited. +var PLpgSQLConditionNameToCode = map[string][]string{ + // Section: Class 00 - Successful Completion + "successful_completion": {"00000"}, + // Section: Class 01 - Warning + "warning": {"01000"}, + "dynamic_result_sets_returned": {"0100C"}, + "implicit_zero_bit_padding": {"01008"}, + "null_value_eliminated_in_set_function": {"01003"}, + "privilege_not_granted": {"01007"}, + "privilege_not_revoked": {"01006"}, + "deprecated_feature": {"01P01"}, + // Section: Class 02 - No Data (this is also a warning class per the SQL standard) + "no_data": {"02000"}, + "no_additional_dynamic_result_sets_returned": {"02001"}, + // Section: Class 03 - SQL Statement Not Yet Complete + "sql_statement_not_yet_complete": {"03000"}, + // Section: Class 08 - Connection Exception + "connection_exception": {"08000"}, + "connection_does_not_exist": {"08003"}, + "connection_failure": {"08006"}, + "sqlclient_unable_to_establish_sqlconnection": {"08001"}, + "sqlserver_rejected_establishment_of_sqlconnection": {"08004"}, + "transaction_resolution_unknown": {"08007"}, + "protocol_violation": {"08P01"}, + // Section: Class 09 - Triggered Action Exception + "triggered_action_exception": {"09000"}, + // Section: Class 0A - Feature Not Supported + "feature_not_supported": {"0A000"}, + // Section: Class 0B - Invalid Transaction Initiation + "invalid_transaction_initiation": {"0B000"}, + // Section: Class 0F - Locator Exception + "locator_exception": {"0F000"}, + "invalid_locator_specification": {"0F001"}, + // Section: Class 0L - Invalid Grantor + "invalid_grantor": {"0L000"}, + "invalid_grant_operation": {"0LP01"}, + // Section: Class 0P - Invalid Role Specification + "invalid_role_specification": {"0P000"}, + // Section: Class 0Z - Diagnostics Exception + "diagnostics_exception": {"0Z000"}, + "stacked_diagnostics_accessed_without_active_handler": {"0Z002"}, + // Section: Class 20 - Case Not Found + "case_not_found": {"20000"}, + // Section: Class 21 - Cardinality Violation + "cardinality_violation": {"21000"}, + // Section: Class 22 - Data Exception + "data_exception": {"22000"}, + "array_subscript_error": {"2202E"}, + "character_not_in_repertoire": {"22021"}, + "datetime_field_overflow": {"22008"}, + "division_by_zero": {"22012"}, + "error_in_assignment": {"22005"}, + "escape_character_conflict": {"2200B"}, + "indicator_overflow": {"22022"}, + "interval_field_overflow": {"22015"}, + "invalid_argument_for_logarithm": {"2201E"}, + "invalid_argument_for_ntile_function": {"22014"}, + "invalid_argument_for_nth_value_function": {"22016"}, + "invalid_argument_for_power_function": {"2201F"}, + "invalid_argument_for_width_bucket_function": {"2201G"}, + "invalid_character_value_for_cast": {"22018"}, + "invalid_datetime_format": {"22007"}, + "invalid_escape_character": {"22019"}, + "invalid_escape_octet": {"2200D"}, + "invalid_escape_sequence": {"22025"}, + "nonstandard_use_of_escape_character": {"22P06"}, + "invalid_indicator_parameter_value": {"22010"}, + "invalid_parameter_value": {"22023"}, + "invalid_regular_expression": {"2201B"}, + "invalid_row_count_in_limit_clause": {"2201W"}, + "invalid_row_count_in_result_offset_clause": {"2201X"}, + "invalid_tablesample_argument": {"2202H"}, + "invalid_tablesample_repeat": {"2202G"}, + "invalid_time_zone_displacement_value": {"22009"}, + "invalid_use_of_escape_character": {"2200C"}, + "most_specific_type_mismatch": {"2200G"}, + "null_value_no_indicator_parameter": {"22002"}, + "numeric_value_out_of_range": {"22003"}, + "string_data_length_mismatch": {"22026"}, + "substring_error": {"22011"}, + "trim_error": {"22027"}, + "unterminated_c_string": {"22024"}, + "zero_length_character_string": {"2200F"}, + "floating_point_exception": {"22P01"}, + "invalid_text_representation": {"22P02"}, + "invalid_binary_representation": {"22P03"}, + "bad_copy_file_format": {"22P04"}, + "untranslatable_character": {"22P05"}, + "not_an_xml_document": {"2200L"}, + "invalid_xml_document": {"2200M"}, + "invalid_xml_content": {"2200N"}, + "invalid_xml_comment": {"2200S"}, + "invalid_xml_processing_instruction": {"2200T"}, + // Section: Class 23 - Integrity Constraint Violation + "integrity_constraint_violation": {"23000"}, + "restrict_violation": {"23001"}, + "not_null_violation": {"23502"}, + "foreign_key_violation": {"23503"}, + "unique_violation": {"23505"}, + "check_violation": {"23514"}, + "exclusion_violation": {"23P01"}, + // Section: Class 24 - Invalid Cursor State + "invalid_cursor_state": {"24000"}, + // Section: Class 25 - Invalid Transaction State + "invalid_transaction_state": {"25000"}, + "active_sql_transaction": {"25001"}, + "branch_transaction_already_active": {"25002"}, + "held_cursor_requires_same_isolation_level": {"25008"}, + "inappropriate_access_mode_for_branch_transaction": {"25003"}, + "inappropriate_isolation_level_for_branch_transaction": {"25004"}, + "no_active_sql_transaction_for_branch_transaction": {"25005"}, + "read_only_sql_transaction": {"25006"}, + "schema_and_data_statement_mixing_not_supported": {"25007"}, + "no_active_sql_transaction": {"25P01"}, + "in_failed_sql_transaction": {"25P02"}, + // Section: Class 26 - Invalid SQL Statement Name + "invalid_sql_statement_name": {"26000"}, + // Section: Class 27 - Triggered Data Change Violation + "triggered_data_change_violation": {"27000"}, + // Section: Class 28 - Invalid Authorization Specification + "invalid_authorization_specification": {"28000"}, + "invalid_password": {"28P01"}, + // Section: Class 2B - Dependent Privilege Descriptors Still Exist + "dependent_privilege_descriptors_still_exist": {"2B000"}, + "dependent_objects_still_exist": {"2BP01"}, + // Section: Class 2D - Invalid Transaction Termination + "invalid_transaction_termination": {"2D000"}, + // Section: Class 2F - SQL Routine Exception + "sql_routine_exception": {"2F000"}, + "function_executed_no_return_statement": {"2F005"}, + // Section: Class 34 - Invalid Cursor Name + "invalid_cursor_name": {"34000"}, + // Section: Class 38 - External Routine Exception + "external_routine_exception": {"38000"}, + "containing_sql_not_permitted": {"38001"}, + // Section: Class 39 - External Routine Invocation Exception + "external_routine_invocation_exception": {"39000"}, + "invalid_sqlstate_returned": {"39001"}, + "trigger_protocol_violated": {"39P01"}, + "srf_protocol_violated": {"39P02"}, + "event_trigger_protocol_violated": {"39P03"}, + // Section: Class 3B - Savepoint Exception + "savepoint_exception": {"3B000"}, + "invalid_savepoint_specification": {"3B001"}, + // Section: Class 3D - Invalid Catalog Name + "invalid_catalog_name": {"3D000"}, + // Section: Class 3F - Invalid Schema Name + "invalid_schema_name": {"3F000"}, + // Section: Class 40 - Transaction Rollback + "transaction_rollback": {"40000"}, + "transaction_integrity_constraint_violation": {"40002"}, + "serialization_failure": {"40001"}, + "statement_completion_unknown": {"40003"}, + "deadlock_detected": {"40P01"}, + // Section: Class 42 - Syntax Error or Access Rule Violation + "syntax_error_or_access_rule_violation": {"42000"}, + "syntax_error": {"42601"}, + "insufficient_privilege": {"42501"}, + "cannot_coerce": {"42846"}, + "grouping_error": {"42803"}, + "windowing_error": {"42P20"}, + "invalid_recursion": {"42P19"}, + "invalid_foreign_key": {"42830"}, + "invalid_name": {"42602"}, + "name_too_long": {"42622"}, + "reserved_name": {"42939"}, + "datatype_mismatch": {"42804"}, + "indeterminate_datatype": {"42P18"}, + "collation_mismatch": {"42P21"}, + "indeterminate_collation": {"42P22"}, + "wrong_object_type": {"42809"}, + "undefined_column": {"42703"}, + "undefined_function": {"42883"}, + "undefined_table": {"42P01"}, + "undefined_parameter": {"42P02"}, + "undefined_object": {"42704"}, + "duplicate_column": {"42701"}, + "duplicate_cursor": {"42P03"}, + "duplicate_database": {"42P04"}, + "duplicate_function": {"42723"}, + "duplicate_prepared_statement": {"42P05"}, + "duplicate_schema": {"42P06"}, + "duplicate_table": {"42P07"}, + "duplicate_alias": {"42712"}, + "duplicate_object": {"42710"}, + "ambiguous_column": {"42702"}, + "ambiguous_function": {"42725"}, + "ambiguous_parameter": {"42P08"}, + "ambiguous_alias": {"42P09"}, + "invalid_column_reference": {"42P10"}, + "invalid_column_definition": {"42611"}, + "invalid_cursor_definition": {"42P11"}, + "invalid_database_definition": {"42P12"}, + "invalid_function_definition": {"42P13"}, + "invalid_prepared_statement_definition": {"42P14"}, + "invalid_schema_definition": {"42P15"}, + "invalid_table_definition": {"42P16"}, + "invalid_object_definition": {"42P17"}, + // Section: Class 44 - WITH CHECK OPTION Violation + "with_check_option_violation": {"44000"}, + // Section: Class 53 - Insufficient Resources + "insufficient_resources": {"53000"}, + "disk_full": {"53100"}, + "out_of_memory": {"53200"}, + "too_many_connections": {"53300"}, + "configuration_limit_exceeded": {"53400"}, + // Section: Class 54 - Program Limit Exceeded + "program_limit_exceeded": {"54000"}, + "statement_too_complex": {"54001"}, + "too_many_columns": {"54011"}, + "too_many_arguments": {"54023"}, + // Section: Class 55 - Object Not In Prerequisite State + "object_not_in_prerequisite_state": {"55000"}, + "object_in_use": {"55006"}, + "cant_change_runtime_param": {"55P02"}, + "lock_not_available": {"55P03"}, + // Section: Class 57 - Operator Intervention + "operator_intervention": {"57000"}, + "query_canceled": {"57014"}, + "admin_shutdown": {"57P01"}, + "crash_shutdown": {"57P02"}, + "cannot_connect_now": {"57P03"}, + "database_dropped": {"57P04"}, + // Section: Class 58 - System Error + "system_error": {"58000"}, + "io_error": {"58030"}, + "undefined_file": {"58P01"}, + "duplicate_file": {"58P02"}, + // Section: Class F0 - Configuration File Error + "config_file_error": {"F0000"}, + "lock_file_exists": {"F0001"}, + // Section: Class HV - Foreign Data Wrapper Error (SQL/MED) + "fdw_error": {"HV000"}, + "fdw_column_name_not_found": {"HV005"}, + "fdw_dynamic_parameter_value_needed": {"HV002"}, + "fdw_function_sequence_error": {"HV010"}, + "fdw_inconsistent_descriptor_information": {"HV021"}, + "fdw_invalid_attribute_value": {"HV024"}, + "fdw_invalid_column_name": {"HV007"}, + "fdw_invalid_column_number": {"HV008"}, + "fdw_invalid_data_type": {"HV004"}, + "fdw_invalid_data_type_descriptors": {"HV006"}, + "fdw_invalid_descriptor_field_identifier": {"HV091"}, + "fdw_invalid_handle": {"HV00B"}, + "fdw_invalid_option_index": {"HV00C"}, + "fdw_invalid_option_name": {"HV00D"}, + "fdw_invalid_string_length_or_buffer_length": {"HV090"}, + "fdw_invalid_string_format": {"HV00A"}, + "fdw_invalid_use_of_null_pointer": {"HV009"}, + "fdw_too_many_handles": {"HV014"}, + "fdw_out_of_memory": {"HV001"}, + "fdw_no_schemas": {"HV00P"}, + "fdw_option_name_not_found": {"HV00J"}, + "fdw_reply_handle": {"HV00K"}, + "fdw_schema_not_found": {"HV00Q"}, + "fdw_table_not_found": {"HV00R"}, + "fdw_unable_to_create_execution": {"HV00L"}, + "fdw_unable_to_create_reply": {"HV00M"}, + "fdw_unable_to_establish_connection": {"HV00N"}, + // Section: Class P0 - PL/pgSQL Error + "plpgsql_error": {"P0000"}, + "raise_exception": {"P0001"}, + "no_data_found": {"P0002"}, + "too_many_rows": {"P0003"}, + "assert_failure": {"P0004"}, + // Section: Class XX - Internal Error + "internal_error": {"XX000"}, + "data_corrupted": {"XX001"}, + "index_corrupted": {"XX002"}, + // Section: Condition names that map to multiple codes. + "null_value_not_allowed": {"22004", "39004"}, + "string_data_right_truncation": {"01004", "22001"}, + "modifying_sql_data_not_permitted": {"2F002", "38002"}, + "prohibited_sql_statement_attempted": {"2F003", "38003"}, + "reading_sql_data_not_permitted": {"2F004", "38004"}, +} diff --git a/pkg/sql/pgwire/pgnotice/display_severity.go b/pkg/sql/pgwire/pgnotice/display_severity.go index f600b863e470..5cb5e761a18b 100644 --- a/pkg/sql/pgwire/pgnotice/display_severity.go +++ b/pkg/sql/pgwire/pgnotice/display_severity.go @@ -34,8 +34,13 @@ const ( // DisplaySeverityNotice is a DisplaySeverity value allowing all notices // of value <= DisplaySeverityNotice to display. DisplaySeverityNotice + // DisplaySeverityInfo is a DisplaySeverity value allowing all notices + // of value <= DisplaySeverityInfo to display. DisplaySeverityInfo is a + // special case in that it will always send a message to the client, no matter + // the value of client_min_messages. + DisplaySeverityInfo // DisplaySeverityLog is a DisplaySeverity value allowing all notices - // of value <= DisplaySeverityLog.g to display. + // of value <= DisplaySeverityLog to display. DisplaySeverityLog // DisplaySeverityDebug1 is a DisplaySeverity value allowing all notices // of value <= DisplaySeverityDebug1 to display. @@ -76,6 +81,7 @@ var noticeDisplaySeverityNames = [...]string{ DisplaySeverityDebug2: "debug2", DisplaySeverityDebug1: "debug1", DisplaySeverityLog: "log", + DisplaySeverityInfo: "info", DisplaySeverityNotice: "notice", DisplaySeverityWarning: "warning", DisplaySeverityError: "error", diff --git a/pkg/sql/planhook.go b/pkg/sql/planhook.go index e719ec9e624c..3bad182335bd 100644 --- a/pkg/sql/planhook.go +++ b/pkg/sql/planhook.go @@ -127,6 +127,7 @@ type PlanHookState interface { SpanConfigReconciler() spanconfig.Reconciler SpanStatsConsumer() keyvisualizer.SpanStatsConsumer BufferClientNotice(ctx context.Context, notice pgnotice.Notice) + SendClientNotice(ctx context.Context, notice pgnotice.Notice) error Txn() *kv.Txn LookupTenantInfo(ctx context.Context, tenantSpec *tree.TenantSpec, op string) (*mtinfopb.TenantInfo, error) GetAvailableTenantID(ctx context.Context, name roachpb.TenantName) (roachpb.TenantID, error) diff --git a/pkg/sql/plpgsql/parser/plpgsql.y b/pkg/sql/plpgsql/parser/plpgsql.y index fa8ce2f44b6a..e8c463b4c9fd 100644 --- a/pkg/sql/plpgsql/parser/plpgsql.y +++ b/pkg/sql/plpgsql/parser/plpgsql.y @@ -129,6 +129,10 @@ func (u *plpgsqlSymUnion) plpgsqlExpr() plpgsqltree.PLpgSQLExpr { return u.val.(plpgsqltree.PLpgSQLExpr) } +func (u *plpgsqlSymUnion) plpgsqlExprs() []plpgsqltree.PLpgSQLExpr { + return u.val.([]plpgsqltree.PLpgSQLExpr) +} + func (u *plpgsqlSymUnion) plpgsqlDecl() *plpgsqltree.PLpgSQLDecl { return u.val.(*plpgsqltree.PLpgSQLDecl) } @@ -137,6 +141,14 @@ func (u *plpgsqlSymUnion) plpgsqlDecls() []plpgsqltree.PLpgSQLDecl { return u.val.([]plpgsqltree.PLpgSQLDecl) } +func (u *plpgsqlSymUnion) plpgsqlOptionExpr() *plpgsqltree.PLpgSQLStmtRaiseOption { + return u.val.(*plpgsqltree.PLpgSQLStmtRaiseOption) +} + +func (u *plpgsqlSymUnion) plpgsqlOptionExprs() []plpgsqltree.PLpgSQLStmtRaiseOption { + return u.val.([]plpgsqltree.PLpgSQLStmtRaiseOption) +} + %} /* * Basic non-keyword token types. These are hard-wired into the core lexer. @@ -299,7 +311,8 @@ func (u *plpgsqlSymUnion) plpgsqlDecls() []plpgsqltree.PLpgSQLDecl { %type <*tree.NumVal> foreach_slice %type for_control -%type any_identifier opt_block_label opt_loop_label opt_label query_options +%type any_identifier opt_block_label opt_loop_label opt_label query_options +%type opt_error_level option_type %type <[]plpgsqltree.PLpgSQLStatement> proc_sect %type <[]*plpgsqltree.PLpgSQLStmtIfElseIfArm> stmt_elsifs @@ -329,6 +342,11 @@ func (u *plpgsqlSymUnion) plpgsqlDecls() []plpgsqltree.PLpgSQLDecl { %type <*plpgsqltree.PLpgSQLStmtGetDiagItem> getdiag_list_item // TODO don't know what this is %type getdiag_item +%type <*plpgsqltree.PLpgSQLStmtRaiseOption> option_expr +%type <[]plpgsqltree.PLpgSQLStmtRaiseOption> option_exprs opt_option_exprs +%type format_expr +%type <[]plpgsqltree.PLpgSQLExpr> opt_format_exprs format_exprs + %type opt_scrollable %type <*plpgsqltree.PLpgSQLStmtFetch> opt_fetch_direction @@ -627,7 +645,9 @@ proc_stmt:pl_block ';' $$.val = $1.plpgsqlStatement() } | stmt_raise - { } + { + $$.val = $1.plpgsqlStatement() + } | stmt_assert { $$.val = $1.plpgsqlStatement() @@ -1017,58 +1037,143 @@ return_variable: expr_until_semi } ; -stmt_raise: RAISE error_level ';' +stmt_raise: + RAISE ';' + { + return unimplemented(plpgsqllex, "empty RAISE statement") + } +| RAISE opt_error_level SCONST opt_format_exprs opt_option_exprs ';' + { + $$.val = &plpgsqltree.PLpgSQLStmtRaise{ + LogLevel: $2, + Message: $3, + Params: $4.plpgsqlExprs(), + Options: $5.plpgsqlOptionExprs(), + } + } +| RAISE opt_error_level IDENT opt_option_exprs ';' + { + $$.val = &plpgsqltree.PLpgSQLStmtRaise{ + LogLevel: $2, + CodeName: $3, + Options: $4.plpgsqlOptionExprs(), + } + } +| RAISE opt_error_level SQLSTATE SCONST opt_option_exprs ';' { + $$.val = &plpgsqltree.PLpgSQLStmtRaise{ + LogLevel: $2, + Code: $4, + Options: $5.plpgsqlOptionExprs(), + } + } +| RAISE opt_error_level USING option_exprs ';' + { + $$.val = &plpgsqltree.PLpgSQLStmtRaise{ + LogLevel: $2, + Options: $4.plpgsqlOptionExprs(), + } } ; -error_level: +opt_error_level: + DEBUG +| LOG +| INFO +| NOTICE +| WARNING +| EXCEPTION +| /* EMPTY */ { - return unimplemented(plpgsqllex, "raise") + $$ = "" } -| EXCEPTION raise_cond option_expr +; + +opt_option_exprs: + USING option_exprs { - return unimplemented(plpgsqllex, "raise exception") + $$.val = $2.plpgsqlOptionExprs() } -| DEBUG raise_cond option_expr +| /* EMPTY */ { - return unimplemented(plpgsqllex, "raise debug") + $$.val = []plpgsqltree.PLpgSQLStmtRaiseOption{} } -| LOG raise_cond option_expr +; + +option_exprs: + option_exprs ',' option_expr { - return unimplemented(plpgsqllex, "raise log") + option := $3.plpgsqlOptionExpr() + $$.val = append($1.plpgsqlOptionExprs(), *option) } -| INFO raise_cond option_expr +| option_expr { - return unimplemented(plpgsqllex, "raise info") + option := $1.plpgsqlOptionExpr() + $$.val = []plpgsqltree.PLpgSQLStmtRaiseOption{*option} } -| WARNING raise_cond option_expr +; + +option_expr: + option_type assign_operator { - return unimplemented(plpgsqllex, "raise warning") + // Read until reaching one of the tokens that can follow a raise option. + sqlStr, _ := plpgsqllex.(*lexer).ReadSqlConstruct(',', ';') + optionExpr, err := plpgsqllex.(*lexer).ParseExpr(sqlStr) + if err != nil { + return setErr(plpgsqllex, err) + } + $$.val = &plpgsqltree.PLpgSQLStmtRaiseOption{ + OptType: $1, + Expr: optionExpr, + } } ; -raise_cond: - {} -| SCONST - {} -| SQLSTATE - {} -| IDENT - {} +option_type: + MESSAGE +| DETAIL +| HINT +| ERRCODE +| COLUMN +| CONSTRAINT +| DATATYPE +| TABLE +| SCHEMA ; +opt_format_exprs: + format_exprs + { + $$.val = $1.plpgsqlExprs() + } + | /* EMPTY */ + { + $$.val = []plpgsqltree.PLpgSQLExpr{} + } +; -option_expr: - {} -| USING MESSAGE assign_operator expr_until_semi - {} -| USING DETAIL assign_operator expr_until_semi - {} -| USING HINT assign_operator expr_until_semi - {} -| USING ERRCODE assign_operator expr_until_semi - {} +format_exprs: + format_expr + { + $$.val = []plpgsqltree.PLpgSQLExpr{$1.plpgsqlExpr()} + } +| format_exprs format_expr + { + $$.val = append($1.plpgsqlExprs(), $2.plpgsqlExpr()) + } +; + +format_expr: ',' + { + // Read until reaching a token that can follow a raise format parameter. + sqlStr, _ := plpgsqllex.(*lexer).ReadSqlConstruct(',', ';', USING) + param, err := plpgsqllex.(*lexer).ParseExpr(sqlStr) + if err != nil { + return setErr(plpgsqllex, err) + } + $$.val = param + } +; stmt_assert: ASSERT assert_cond ';' { @@ -1083,6 +1188,7 @@ assert_cond: plpgsqllex.(*lexer).ReadSqlExpressionStr(';') } } +; loop_body: proc_sect END LOOP { diff --git a/pkg/sql/plpgsql/parser/testdata/stmt_raise b/pkg/sql/plpgsql/parser/testdata/stmt_raise index 544b49a44d62..e4df15b04a52 100644 --- a/pkg/sql/plpgsql/parser/testdata/stmt_raise +++ b/pkg/sql/plpgsql/parser/testdata/stmt_raise @@ -6,15 +6,17 @@ END ---- expected parse error: at or near ";": syntax error: unimplemented: this syntax - parse DECLARE BEGIN RAISE EXCEPTION USING MESSAGE = "why is this so involved?"; END ---- -expected parse error: at or near "why is this so involved?": syntax error: unimplemented: this syntax - +DECLARE +BEGIN +RAISE exception +USING MESSAGE = "why is this so involved?"; +END parse DECLARE @@ -22,7 +24,11 @@ BEGIN RAISE LOG USING HINT = "Insert HINT"; END ---- -expected parse error: at or near "Insert HINT": syntax error: unimplemented: this syntax +DECLARE +BEGIN +RAISE log +USING HINT = "Insert HINT"; +END parse DECLARE @@ -30,7 +36,10 @@ BEGIN RAISE LOG 'Nonexistent ID --> %', user_id; END ---- -expected parse error: at or near ",": syntax error: unimplemented: this syntax +DECLARE +BEGIN +RAISE log 'Nonexistent ID --> %', user_id; +END parse DECLARE @@ -39,17 +48,73 @@ BEGIN USING HINT = "check...userid?" ; END ---- -expected parse error: at or near ",": syntax error: unimplemented: this syntax +DECLARE +BEGIN +RAISE log 'Nonexistent ID --> %', user_id +USING HINT = "check...userid?"; +END +parse +DECLARE +BEGIN + RAISE 'foo %', 'bar'; +END +---- +DECLARE +BEGIN +RAISE 'foo %', 'bar'; +END parse DECLARE + i INT := 0; BEGIN - RAISE SQLSTATE '222222' USING HINT = "hm"; + RAISE 'foo %', i; END ---- -expected parse error: at or near "sqlstate": syntax error: unimplemented: this syntax +DECLARE +i INT8 := 0; +BEGIN +RAISE 'foo %', i; +END +parse +DECLARE + i INT := 0; +BEGIN + RAISE 'foo %, %, %.', i, i*2, i*100; +END +---- +DECLARE +i INT8 := 0; +BEGIN +RAISE 'foo %, %, %.', i, i * 2, i * 100; +END + +parse +DECLARE + i INT := 0; +BEGIN + RAISE 'foo %', (SELECT count(*) FROM xy WHERE x = i); +END +---- +DECLARE +i INT8 := 0; +BEGIN +RAISE 'foo %', (SELECT count(*) FROM xy WHERE x = i); +END + +parse +DECLARE +BEGIN + RAISE SQLSTATE '222222' USING HINT = "hm"; +END +---- +DECLARE +BEGIN +RAISE SQLSTATE '222222' +USING HINT = hm; +END parse DECLARE @@ -57,4 +122,22 @@ BEGIN RAISE internal_screaming; END ---- -expected parse error: at or near "internal_screaming": syntax error: unimplemented: this syntax +DECLARE +BEGIN +RAISE internal_screaming; +END + +parse +DECLARE +BEGIN + RAISE internal_screaming + USING MESSAGE = 'blah blah blah', + COLUMN = 'foo'; +END +---- +DECLARE +BEGIN +RAISE internal_screaming +USING MESSAGE = 'blah blah blah', +COLUMN = 'foo'; +END diff --git a/pkg/sql/sem/builtins/builtins.go b/pkg/sql/sem/builtins/builtins.go index 6c8150036edb..4997d6ad2a4d 100644 --- a/pkg/sql/sem/builtins/builtins.go +++ b/pkg/sql/sem/builtins/builtins.go @@ -27,6 +27,7 @@ import ( "math/bits" "math/rand" "net" + "regexp" "regexp/syntax" "strconv" "strings" @@ -5377,7 +5378,7 @@ SELECT return nil, errors.Newf("expected string value, got %T", args[0]) } msg := string(s) - return crdbInternalSendNotice(ctx, evalCtx, "NOTICE", msg) + return crdbInternalBufferNotice(ctx, evalCtx, "NOTICE", msg) }, Info: "This function is used only by CockroachDB's developers for testing purposes.", Volatility: volatility.Volatile, @@ -5399,7 +5400,7 @@ SELECT if _, ok := pgnotice.ParseDisplaySeverity(severityString); !ok { return nil, pgerror.Newf(pgcode.InvalidParameterValue, "severity %s is invalid", severityString) } - return crdbInternalSendNotice(ctx, evalCtx, severityString, msg) + return crdbInternalBufferNotice(ctx, evalCtx, severityString, msg) }, Info: "This function is used only by CockroachDB's developers for testing purposes.", Volatility: volatility.Volatile, @@ -8098,6 +8099,77 @@ expires until the statement bundle is collected`, Volatility: volatility.Immutable, }, ), + "crdb_internal.plpgsql_raise": makeBuiltin(tree.FunctionProperties{ + Category: builtinconstants.CategoryString, + Undocumented: true, + }, + tree.Overload{ + Types: tree.ParamTypes{ + {Name: "severity", Typ: types.String}, + {Name: "message", Typ: types.String}, + {Name: "detail", Typ: types.String}, + {Name: "hint", Typ: types.String}, + {Name: "code", Typ: types.String}, + }, + ReturnType: tree.FixedReturnType(types.Int), + Fn: func(ctx context.Context, evalCtx *eval.Context, args tree.Datums) (tree.Datum, error) { + argStrings := make([]string, len(args)) + for i := range args { + s, ok := tree.AsDString(args[i]) + if !ok { + return nil, errors.Newf("expected string value, got %T", args[i]) + } + argStrings[i] = string(s) + } + // Build the error. + severity := strings.ToUpper(argStrings[0]) + if _, ok := pgnotice.ParseDisplaySeverity(severity); !ok { + return nil, pgerror.Newf( + pgcode.InvalidParameterValue, "severity %s is invalid", severity, + ) + } + message := argStrings[1] + err := errors.Newf("%s", message) + err = pgerror.WithSeverity(err, severity) + if detail := argStrings[2]; detail != "" { + err = errors.WithDetail(err, detail) + } + if hint := argStrings[3]; hint != "" { + err = errors.WithHint(err, hint) + } + if codeString := argStrings[4]; codeString != "" { + var code string + if regexp.MustCompile(`[A-Z0-9]{5}`).MatchString(codeString) { + // The supplied argument is a valid PG code. + code = codeString + } else { + // The supplied string may be a condition name. + if candidates, ok := pgcode.PLpgSQLConditionNameToCode[codeString]; ok { + // Some condition names map to more than one code, but postgres + // seems to just use the first (smallest) one. + code = candidates[0] + } else { + return nil, pgerror.Newf(pgcode.UndefinedObject, + "unrecognized exception condition: \"%s\"", codeString, + ) + } + } + err = pgerror.WithCandidateCode(err, pgcode.MakeCode(code)) + } + if severity == "ERROR" { + // Directly return the error from the function call. + return nil, err + } + // Send the error as a notice to the client, then return NULL. + if sendErr := crdbInternalSendNotice(ctx, evalCtx, err); sendErr != nil { + return nil, sendErr + } + return tree.DNull, nil + }, + Info: "This function is used internally to implement the PLpgSQL RAISE statement.", + Volatility: volatility.Volatile, + }, + ), } var lengthImpls = func(incBitOverload bool) builtinDefinition { diff --git a/pkg/sql/sem/builtins/fixed_oids.go b/pkg/sql/sem/builtins/fixed_oids.go index 6a745c051f7d..1238ff66e360 100644 --- a/pkg/sql/sem/builtins/fixed_oids.go +++ b/pkg/sql/sem/builtins/fixed_oids.go @@ -2431,6 +2431,7 @@ var builtinOidsArray = []string{ 2458: `pg_sequence_last_value(sequence_oid: oid) -> int`, 2459: `nameconcatoid(name: string, oid: oid) -> name`, 2460: `pg_get_function_arg_default(func_oid: oid, arg_num: int4) -> string`, + 2461: `crdb_internal.plpgsql_raise(severity: string, message: string, detail: string, hint: string, code: string) -> int`, } var builtinOidsBySignature map[string]oid.Oid diff --git a/pkg/sql/sem/builtins/notice.go b/pkg/sql/sem/builtins/notice.go index 3e4ff0520bc1..2a834df24414 100644 --- a/pkg/sql/sem/builtins/notice.go +++ b/pkg/sql/sem/builtins/notice.go @@ -20,10 +20,11 @@ import ( "github.com/cockroachdb/errors" ) -// crdbInternalSendNotice sends a notice. +// crdbInternalBufferNotice sends a notice that will be buffered until the +// connection is closed. // Note this is extracted to a different file to prevent churn on the pgwire // test, which records line numbers. -func crdbInternalSendNotice( +func crdbInternalBufferNotice( ctx context.Context, evalCtx *eval.Context, severity string, msg string, ) (tree.Datum, error) { if evalCtx.ClientNoticeSender == nil { @@ -35,3 +36,11 @@ func crdbInternalSendNotice( ) return tree.NewDInt(0), nil } + +// crdbInternalSendNotice immediately flushes a notice to the client. +func crdbInternalSendNotice(ctx context.Context, evalCtx *eval.Context, err error) error { + if evalCtx.ClientNoticeSender == nil { + return errors.AssertionFailedf("notice sender not set") + } + return evalCtx.ClientNoticeSender.SendClientNotice(ctx, pgnotice.Notice(err)) +} diff --git a/pkg/sql/sem/eval/deps.go b/pkg/sql/sem/eval/deps.go index fa9d3d6b8a78..8deef15f3245 100644 --- a/pkg/sql/sem/eval/deps.go +++ b/pkg/sql/sem/eval/deps.go @@ -499,6 +499,10 @@ type ClientNoticeSender interface { // BufferClientNotice buffers the notice to send to the client. // This is flushed before the connection is closed. BufferClientNotice(ctx context.Context, notice pgnotice.Notice) + // SendClientNotice immediately flushes the notice to the client. This is used + // to implement PLpgSQL RAISE statements; most cases should use + // BufferClientNotice. + SendClientNotice(ctx context.Context, notice pgnotice.Notice) error } // DeferredRoutineSender allows a nested routine to send the information needed diff --git a/pkg/sql/sem/plpgsqltree/constants.go b/pkg/sql/sem/plpgsqltree/constants.go index b0036d2abfb6..7b7dd2e9a9b3 100644 --- a/pkg/sql/sem/plpgsqltree/constants.go +++ b/pkg/sql/sem/plpgsqltree/constants.go @@ -12,10 +12,6 @@ package plpgsqltree import "github.com/cockroachdb/errors" -// PLpgSQLRaiseOptionType represents the severity of the error in -// a raise statement. -type PLpgSQLRaiseOptionType int - // PLpgSQLGetDiagKind represents the type of error diagnostic // item in stmt_getdiag. type PLpgSQLGetDiagKind int diff --git a/pkg/sql/sem/plpgsqltree/statements.go b/pkg/sql/sem/plpgsqltree/statements.go index 3f77feb23822..04584cc8418a 100644 --- a/pkg/sql/sem/plpgsqltree/statements.go +++ b/pkg/sql/sem/plpgsqltree/statements.go @@ -12,6 +12,7 @@ package plpgsqltree import ( "fmt" + "strings" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" ) @@ -603,7 +604,8 @@ func (s *PLpgSQLStmtReturnQuery) WalkStmt(visitor PLpgSQLStmtVisitor) { // stmt_raise type PLpgSQLStmtRaise struct { PLpgSQLStatementImpl - LogLevel int + LogLevel string + Code string CodeName string Message string Params []PLpgSQLExpr @@ -611,14 +613,43 @@ type PLpgSQLStmtRaise struct { } func (s *PLpgSQLStmtRaise) Format(ctx *tree.FmtCtx) { + ctx.WriteString("RAISE") + if s.LogLevel != "" { + ctx.WriteString(" ") + ctx.WriteString(s.LogLevel) + } + if s.Code != "" { + ctx.WriteString(fmt.Sprintf(" SQLSTATE '%s'", s.Code)) + } + if s.CodeName != "" { + ctx.WriteString(fmt.Sprintf(" %s", s.CodeName)) + } + if s.Message != "" { + ctx.WriteString(fmt.Sprintf(" '%s'", s.Message)) + for i := range s.Params { + ctx.WriteString(", ") + s.Params[i].Format(ctx) + } + } + for i := range s.Options { + if i == 0 { + ctx.WriteString("\nUSING ") + } else { + ctx.WriteString(",\n") + } + s.Options[i].Format(ctx) + } + ctx.WriteString(";\n") } type PLpgSQLStmtRaiseOption struct { - OptType PLpgSQLRaiseOptionType + OptType string Expr PLpgSQLExpr } func (s *PLpgSQLStmtRaiseOption) Format(ctx *tree.FmtCtx) { + ctx.WriteString(fmt.Sprintf("%s = ", strings.ToUpper(s.OptType))) + s.Expr.Format(ctx) } func (s *PLpgSQLStmtRaise) PlpgSQLStatementTag() string { diff --git a/pkg/sql/sem/tree/eval.go b/pkg/sql/sem/tree/eval.go index 82ce81024e82..586b55a2778c 100644 --- a/pkg/sql/sem/tree/eval.go +++ b/pkg/sql/sem/tree/eval.go @@ -2052,7 +2052,7 @@ func (e *MultipleResultsError) Error() string { func (expr *FuncExpr) MaybeWrapError(err error) error { // If we are facing an explicit error, propagate it unchanged. fName := expr.Func.String() - if fName == `crdb_internal.force_error` { + if fName == `crdb_internal.force_error` || fName == `crdb_internal.plpgsql_raise` { return err } // Otherwise, wrap it with context.