From c73c32e36fce79ebc424184e3f560bd460c5f2b0 Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Thu, 29 Jun 2023 23:46:57 -0600 Subject: [PATCH 1/3] builtins: add builtin function to raise a notice synchronously to the client This patch adds a builtin function, `crdb_internal.plpgsql_raise`, which allows the caller to send a notice to the client with specified severity, message, detail, hint, and PG code. The notice is immediately flushed to the client instead of being buffered until the query result is closed. This functionality will be used to implement the PLpgSQL `RAISE` statement. The `crdb_internal.plpgsql_raise` builtin is undocumented and intended only for internal use. Informs #105251 Release note: None --- .../tests/3node-tenant/generated_test.go | 7 + pkg/sql/conn_io.go | 9 + pkg/sql/faketreeeval/evalctx.go | 5 + pkg/sql/logictest/logic.go | 3 + pkg/sql/logictest/testdata/logic_test/notice | 3 +- pkg/sql/logictest/testdata/logic_test/raise | 166 ++++++++++ .../tests/fakedist-disk/generated_test.go | 7 + .../tests/fakedist-vec-off/generated_test.go | 7 + .../tests/fakedist/generated_test.go | 7 + .../generated_test.go | 7 + .../local-mixed-22.2-23.1/generated_test.go | 7 + .../tests/local-vec-off/generated_test.go | 7 + .../logictest/tests/local/generated_test.go | 7 + pkg/sql/notice.go | 43 ++- pkg/sql/pgwire/command_result.go | 8 + pkg/sql/pgwire/pgcode/BUILD.bazel | 1 + pkg/sql/pgwire/pgcode/generate_names.sh | 15 + pkg/sql/pgwire/pgcode/plpgsql_codenames.go | 294 ++++++++++++++++++ pkg/sql/pgwire/pgnotice/display_severity.go | 8 +- pkg/sql/planhook.go | 1 + pkg/sql/sem/builtins/builtins.go | 76 ++++- pkg/sql/sem/builtins/fixed_oids.go | 1 + pkg/sql/sem/builtins/notice.go | 13 +- pkg/sql/sem/eval/deps.go | 4 + pkg/sql/sem/tree/eval.go | 2 +- 25 files changed, 691 insertions(+), 17 deletions(-) create mode 100644 pkg/sql/logictest/testdata/logic_test/raise create mode 100644 pkg/sql/pgwire/pgcode/generate_names.sh create mode 100644 pkg/sql/pgwire/pgcode/plpgsql_codenames.go 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/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/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/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 a0b3b551f3d2..6a2de0689eac 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 } // PrivilegedAccessor gives access to certain queries that would otherwise 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. From 4f8055ded339fba7a41613af31894892a82fb21b Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Wed, 5 Jul 2023 17:59:49 -0600 Subject: [PATCH 2/3] plpgsql: implement parsing for RAISE statements This patch adds parser support for PLpgSQL `RAISE` statements. This includes all syntax forms apart from the empty `RAISE`, which is only valid in combination with (currently unimplemented) `EXCEPTION` blocks. A future commit will add support in the optbuilder as well. Informs #105251 Release note: None --- pkg/sql/plpgsql/parser/plpgsql.y | 172 +++++++++++++++++---- pkg/sql/plpgsql/parser/testdata/stmt_raise | 101 ++++++++++-- pkg/sql/sem/plpgsqltree/constants.go | 4 - pkg/sql/sem/plpgsqltree/statements.go | 35 ++++- 4 files changed, 264 insertions(+), 48 deletions(-) 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/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 { From 6de64155b4002cdad46fe36fd93fbfac27b41082 Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Wed, 5 Jul 2023 18:00:10 -0600 Subject: [PATCH 3/3] plpgsql: implement RAISE statement This patch adds support for the PLpgSQL `RAISE` statement. The `RAISE` statement can send messages back to the client during execution, as well as raise a user-specified error. There are a few variations on the syntax, but in general `RAISE` statements have a log level (default `EXCEPTION`), a message (if not specified, the code string is used), and various options: `DETAIL`, `HINT`, `ERRCODE` etc. With log level `EXCEPTION` the error is returned just like any other error, but for other levels it is sent as a notice to the client and flushed synchronously before execution continues. This feature is often used to track progress, since the notices are sent before execution finishes. Fixes #105251 Release note (sql change): Added support for the PLpgSQL `RAISE` statement, which allows sending notices to the client and raising errors. Currently the notice is only sent to the client; support for logging notices is left for future work. --- .../logictest/testdata/logic_test/udf_plpgsql | 272 +++ pkg/sql/opt/memo/expr.go | 12 +- pkg/sql/opt/optbuilder/plpgsql.go | 217 ++- pkg/sql/opt/optbuilder/testdata/udf_plpgsql | 1541 +++++++++++++++++ 4 files changed, 2016 insertions(+), 26 deletions(-) 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/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 907dfc69ec10..006f965b97b0 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 @@ -513,6 +686,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