From 58b34e7c7da898c6f40bf9527526c3f44ae6c051 Mon Sep 17 00:00:00 2001 From: Frederick Date: Tue, 8 Oct 2024 09:45:23 -0700 Subject: [PATCH] Add a warning system (#321) See [edgedb issue #7822](https://github.com/edgedb/edgedb/issues/7822). --- export.go | 12 ++++++ internal/client/cache.go | 2 +- internal/client/client.go | 49 +++++++++++++++++++--- internal/client/granularflow0pX.go | 14 +++---- internal/client/granularflow1pX.go | 14 +++---- internal/client/granularflow2pX.go | 18 ++++---- internal/client/options.go | 17 ++++++++ internal/client/query.go | 67 +++++++++++++++++------------- internal/client/query_test.go | 53 +++++++++++++++++++++++ internal/client/scriptflow.go | 46 +++++++++++++++++--- internal/client/transactable.go | 2 + internal/client/transaction.go | 51 ++++++++++++++++++++--- internal/client/warning.go | 43 +++++++++++++++++++ internal/cmd/export/names.txt | 3 ++ internal/header/header.go | 7 +++- rstdocs/api.rst | 15 ++++++- 16 files changed, 341 insertions(+), 72 deletions(-) create mode 100644 internal/client/warning.go diff --git a/export.go b/export.go index 553cef8..7b4f342 100644 --- a/export.go +++ b/export.go @@ -270,6 +270,11 @@ type ( // UUID is a universally unique identifier // https://www.edgedb.com/docs/stdlib/uuid UUID = edgedbtypes.UUID + + // WarningHandler takes a slice of edgedb.Error that represent warnings and + // optionally returns an error. This can be used to log warnings, increment + // metrics, promote warnings to errors by returning them etc. + WarningHandler = edgedb.WarningHandler ) var ( @@ -292,6 +297,9 @@ var ( // from a [time.Duration] represented as nanoseconds. DurationFromNanoseconds = edgedbtypes.DurationFromNanoseconds + // LogWarnings is an edgedb.WarningHandler that logs warnings. + LogWarnings = edgedb.LogWarnings + // NewDateDuration returns a new DateDuration NewDateDuration = edgedbtypes.NewDateDuration @@ -439,4 +447,8 @@ var ( // ParseUUID parses s into a UUID or returns an error. ParseUUID = edgedbtypes.ParseUUID + + // WarningsAsErrors is an edgedb.WarningHandler that returns warnings as + // errors. + WarningsAsErrors = edgedb.WarningsAsErrors ) diff --git a/internal/client/cache.go b/internal/client/cache.go index 698d576..c327791 100644 --- a/internal/client/cache.go +++ b/internal/client/cache.go @@ -98,7 +98,7 @@ func (c *protocolConnection) cacheTypeIDs(q *query, ids idPair) { func (c *protocolConnection) cacheCapabilities0pX( q *query, - headers header.Header, + headers header.Header0pX, ) { if capabilities, ok := headers[header.Capabilities]; ok { x := binary.BigEndian.Uint64(capabilities) diff --git a/internal/client/client.go b/internal/client/client.go index 1fe1921..56f983e 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -58,6 +58,8 @@ type Client struct { cfg *connConfig cacheCollection state map[string]interface{} + + warningHandler WarningHandler } // CreateClient returns a new client. The client connects lazily. Call @@ -81,6 +83,11 @@ func CreateClientDSN(_ context.Context, dsn string, opts Options) (*Client, erro return nil, err } + warningHandler := LogWarnings + if opts.WarningHandler != nil { + warningHandler = opts.WarningHandler + } + False := false p := &Client{ isClosed: &False, @@ -98,7 +105,8 @@ func CreateClientDSN(_ context.Context, dsn string, opts Options) (*Client, erro outCodecCache: cache.New(1_000), capabilitiesCache: cache.New(1_000), }, - state: make(map[string]interface{}), + state: make(map[string]interface{}), + warningHandler: warningHandler, } return p, nil @@ -326,6 +334,7 @@ func (p *Client) Execute( copyState(p.state), nil, true, + p.warningHandler, ) if err != nil { return err @@ -347,7 +356,8 @@ func (p *Client) Query( return err } - err = runQuery(ctx, conn, "Query", cmd, out, args, p.state) + err = runQuery( + ctx, conn, "Query", cmd, out, args, p.state, p.warningHandler) return firstError(err, p.release(conn, err)) } @@ -366,7 +376,16 @@ func (p *Client) QuerySingle( return err } - err = runQuery(ctx, conn, "QuerySingle", cmd, out, args, p.state) + err = runQuery( + ctx, + conn, + "QuerySingle", + cmd, + out, + args, + p.state, + p.warningHandler, + ) return firstError(err, p.release(conn, err)) } @@ -382,7 +401,16 @@ func (p *Client) QueryJSON( return err } - err = runQuery(ctx, conn, "QueryJSON", cmd, out, args, p.state) + err = runQuery( + ctx, + conn, + "QueryJSON", + cmd, + out, + args, + p.state, + p.warningHandler, + ) return firstError(err, p.release(conn, err)) } @@ -400,7 +428,16 @@ func (p *Client) QuerySingleJSON( return err } - err = runQuery(ctx, conn, "QuerySingleJSON", cmd, out, args, p.state) + err = runQuery( + ctx, + conn, + "QuerySingleJSON", + cmd, + out, + args, + p.state, + p.warningHandler, + ) return firstError(err, p.release(conn, err)) } @@ -424,6 +461,6 @@ func (p *Client) Tx(ctx context.Context, action TxBlock) error { return err } - err = conn.tx(ctx, action, p.state) + err = conn.tx(ctx, action, p.state, p.warningHandler) return firstError(err, p.release(conn, err)) } diff --git a/internal/client/granularflow0pX.go b/internal/client/granularflow0pX.go index bd725bc..df32b52 100644 --- a/internal/client/granularflow0pX.go +++ b/internal/client/granularflow0pX.go @@ -171,7 +171,7 @@ func (c *protocolConnection) prepare0pX(r *buff.Reader, q *query) error { w := buff.NewWriter(c.writeMemory[:0]) w.BeginMessage(uint8(Parse)) - writeHeaders(w, headers) + writeHeaders0pX(w, headers) w.PushUint8(uint8(q.fmt)) w.PushUint8(uint8(q.expCard)) w.PushUint32(0) // no statement name @@ -194,7 +194,7 @@ func (c *protocolConnection) prepare0pX(r *buff.Reader, q *query) error { for r.Next(done.Chan) { switch Message(r.MsgType) { case ParseComplete: - c.cacheCapabilities0pX(q, decodeHeaders(r)) + c.cacheCapabilities0pX(q, decodeHeaders0pX(r)) r.Discard(1) // cardinality ids := idPair{in: r.PopUUID(), out: r.PopUUID()} c.cacheTypeIDs(q, ids) @@ -269,7 +269,7 @@ func (c *protocolConnection) execute0pX( ) error { w := buff.NewWriter(c.writeMemory[:0]) w.BeginMessage(uint8(Execute0pX)) - writeHeaders(w, q.headers0pX()) + writeHeaders0pX(w, q.headers0pX()) w.PushUint32(0) // no statement name if e := cdcs.in.Encode(w, q.args, codecs.Path("args"), true); e != nil { return &invalidArgumentError{msg: e.Error()} @@ -348,7 +348,7 @@ func (c *protocolConnection) optimistic0pX( w := buff.NewWriter(c.writeMemory[:0]) w.BeginMessage(uint8(Execute)) - writeHeaders(w, headers) + writeHeaders0pX(w, headers) w.PushUint8(uint8(q.fmt)) w.PushUint8(uint8(q.expCard)) w.PushString(q.cmd) @@ -396,7 +396,7 @@ func (c *protocolConnection) optimistic0pX( decodeCommandCompleteMsg0pX(r) case CommandDataDescription: var ( - headers header.Header + headers header.Header0pX e error ) @@ -482,8 +482,8 @@ func decodeDataMsg( func (c *protocolConnection) decodeCommandDataDescriptionMsg0pX( r *buff.Reader, q *query, -) (*CommandDescription, header.Header, error) { - headers := decodeHeaders(r) +) (*CommandDescription, header.Header0pX, error) { + headers := decodeHeaders0pX(r) card := r.PopUint8() var ( diff --git a/internal/client/granularflow1pX.go b/internal/client/granularflow1pX.go index 6b4e0b9..08dd946 100644 --- a/internal/client/granularflow1pX.go +++ b/internal/client/granularflow1pX.go @@ -125,14 +125,14 @@ func (c *protocolConnection) decodeCommandDataDescriptionMsg1pX( r *buff.Reader, q *query, ) (*CommandDescription, error) { - discardHeaders(r) - c.cacheCapabilities1pX(q, r.PopUint64()) + _, err := decodeHeaders1pX(r, q.warningHandler) + if err != nil { + return nil, err + } - var ( - err error - descs CommandDescription - ) + c.cacheCapabilities1pX(q, r.PopUint64()) + var descs CommandDescription descs.Card = Cardinality(r.PopUint8()) id := r.PopUUID() descs.In, err = descriptor.Pop( @@ -323,7 +323,7 @@ func (c *protocolConnection) decodeCommandCompleteMsg1pX( q *query, r *buff.Reader, ) error { - discardHeaders(r) + discardHeaders0pX(r) c.cacheCapabilities1pX(q, r.PopUint64()) r.Discard(int(r.PopUint32())) // discard command status if r.PopUUID() == descriptor.IDZero { diff --git a/internal/client/granularflow2pX.go b/internal/client/granularflow2pX.go index 425414f..cb2aae3 100644 --- a/internal/client/granularflow2pX.go +++ b/internal/client/granularflow2pX.go @@ -131,14 +131,14 @@ func (c *protocolConnection) decodeCommandDataDescriptionMsg2pX( r *buff.Reader, q *query, ) (*CommandDescriptionV2, error) { - discardHeaders(r) - c.cacheCapabilities1pX(q, r.PopUint64()) + _, err := decodeHeaders2pX(r, q.warningHandler) + if err != nil { + return nil, err + } - var ( - err error - descs CommandDescriptionV2 - ) + c.cacheCapabilities1pX(q, r.PopUint64()) + var descs CommandDescriptionV2 descs.Card = Cardinality(r.PopUint8()) id := r.PopUUID() descs.In, err = descriptor.PopV2( @@ -231,9 +231,9 @@ func (c *protocolConnection) execute2pX( err = wrapAll(err, e) } case CommandDataDescription: - descs, e := c.decodeCommandDataDescriptionMsg1pX(r, q) + descs, e := c.decodeCommandDataDescriptionMsg2pX(r, q) err = wrapAll(err, e) - cdcs, e = c.codecsFromDescriptors1pX(q, descs) + cdcs, e = c.codecsFromDescriptors2pX(q, descs) err = wrapAll(err, e) case Data: val, ok, e := decodeDataMsg(r, q, cdcs) @@ -367,7 +367,7 @@ func (c *protocolConnection) decodeCommandCompleteMsg2pX( q *query, r *buff.Reader, ) error { - discardHeaders(r) + discardHeaders0pX(r) c.cacheCapabilities1pX(q, r.PopUint64()) r.Discard(int(r.PopUint32())) // discard command status if r.PopUUID() == descriptor.IDZero { diff --git a/internal/client/options.go b/internal/client/options.go index 9b35bb4..af76ff2 100644 --- a/internal/client/options.go +++ b/internal/client/options.go @@ -116,6 +116,10 @@ type Options struct { // SecretKey is used to connect to cloud instances. SecretKey string + + // WarningHandler is invoked when EdgeDB returns warnings. Defaults to + // edgedb.LogWarnings. + WarningHandler WarningHandler } // TLSOptions contains the parameters needed to configure TLS on EdgeDB @@ -512,3 +516,16 @@ func (p Client) WithoutGlobals(globals ...string) *Client { // nolint:gocritic p.state = state return &p } + +// WithWarningHandler sets the warning handler for the returned client. If +// warningHandler is nil edgedb.LogWarnings is used. +func (p Client) WithWarningHandler( // nolint:gocritic + warningHandler WarningHandler, +) *Client { + if warningHandler == nil { + warningHandler = LogWarnings + } + + p.warningHandler = warningHandler + return &p +} diff --git a/internal/client/query.go b/internal/client/query.go index 033ee76..8cb16e9 100644 --- a/internal/client/query.go +++ b/internal/client/query.go @@ -28,17 +28,23 @@ import ( "github.com/edgedb/edgedb-go/internal/introspect" ) +// WarningHandler takes a slice of edgedb.Error that represent warnings and +// optionally returns an error. This can be used to log warnings, increment +// metrics, promote warnings to errors by returning them etc. +type WarningHandler = func([]error) error + type query struct { - out reflect.Value - outType reflect.Type - method string - cmd string - fmt Format - expCard Cardinality - args []interface{} - capabilities uint64 - state map[string]interface{} - parse bool + out reflect.Value + outType reflect.Type + method string + cmd string + fmt Format + expCard Cardinality + args []interface{} + capabilities uint64 + state map[string]interface{} + parse bool + warningHandler WarningHandler } func (q *query) flat() bool { @@ -53,11 +59,11 @@ func (q *query) flat() bool { return false } -func (q *query) headers0pX() header.Header { +func (q *query) headers0pX() header.Header0pX { bts := make([]byte, 8) binary.BigEndian.PutUint64(bts, q.capabilities) - return header.Header{header.AllowCapabilities: bts} + return header.Header0pX{header.AllowCapabilities: bts} } // newQuery returns a new granular flow query. @@ -68,6 +74,7 @@ func newQuery( state map[string]interface{}, out interface{}, parse bool, + warningHandler WarningHandler, ) (*query, error) { var ( expCard Cardinality @@ -77,14 +84,15 @@ func newQuery( switch method { case "Execute": return &query{ - method: method, - cmd: cmd, - fmt: Null, - expCard: Many, - args: args, - capabilities: capabilities, - state: state, - parse: parse, + method: method, + cmd: cmd, + fmt: Null, + expCard: Many, + args: args, + capabilities: capabilities, + state: state, + parse: parse, + warningHandler: warningHandler, }, nil case "Query": expCard = Many @@ -103,14 +111,15 @@ func newQuery( } q := query{ - method: method, - cmd: cmd, - fmt: frmt, - expCard: expCard, - args: args, - capabilities: capabilities, - state: state, - parse: parse, + method: method, + cmd: cmd, + fmt: frmt, + expCard: expCard, + args: args, + capabilities: capabilities, + state: state, + parse: parse, + warningHandler: warningHandler, } var err error @@ -152,6 +161,7 @@ func runQuery( out interface{}, args []interface{}, state map[string]interface{}, + warningHandler WarningHandler, ) error { if method == "QuerySingleJSON" { switch out.(type) { @@ -171,6 +181,7 @@ func runQuery( state, out, true, + warningHandler, ) if err != nil { return err diff --git a/internal/client/query_test.go b/internal/client/query_test.go index 8b7e29d..0455622 100644 --- a/internal/client/query_test.go +++ b/internal/client/query_test.go @@ -1126,3 +1126,56 @@ func TestWithConfigWrongServerVersion(t *testing.T) { "are not supported by the server. "+ "Upgrade your server to version 2.0 or greater to use these features.") } + +func TestWithWarningHandler(t *testing.T) { + var hasWarnOnCall bool + ctx := context.Background() + err := client.QuerySingle( + ctx, + ` + SELECT EXISTS ( + SELECT schema::Function { id } + FILTER .name = 'std::_warn_on_call' + ) + `, + &hasWarnOnCall, + ) + require.NoError(t, err) + + if !hasWarnOnCall { + t.Skip() + } + + seen := []error{} + a := client.WithWarningHandler(func(warnings []error) error { + seen = append(seen, warnings...) + return nil + }) + + err = a.Execute(ctx, `SELECT _warn_on_call()`) + require.NoError(t, err) + require.Greater(t, len(seen), 0) + + var resultMany []int64 + seen = []error{} + err = a.Query(ctx, `SELECT _warn_on_call()`, &resultMany) + require.NoError(t, err) + require.Greater(t, len(seen), 0) + + var resultJSON []byte + seen = []error{} + err = a.QueryJSON(ctx, `SELECT _warn_on_call()`, &resultJSON) + require.NoError(t, err) + require.Greater(t, len(seen), 0) + + var resultSingle int64 + seen = []error{} + err = a.QuerySingle(ctx, `SELECT _warn_on_call()`, &resultSingle) + require.NoError(t, err) + require.Greater(t, len(seen), 0) + + seen = []error{} + err = a.QuerySingleJSON(ctx, `SELECT _warn_on_call()`, &resultJSON) + require.NoError(t, err) + require.Greater(t, len(seen), 0) +} diff --git a/internal/client/scriptflow.go b/internal/client/scriptflow.go index 7284211..959f9f3 100644 --- a/internal/client/scriptflow.go +++ b/internal/client/scriptflow.go @@ -17,6 +17,8 @@ package edgedb import ( + "encoding/json" + "github.com/edgedb/edgedb-go/internal/buff" "github.com/edgedb/edgedb-go/internal/header" ) @@ -30,10 +32,10 @@ func ignoreHeaders(r *buff.Reader) { } } -func decodeHeaders(r *buff.Reader) header.Header { +func decodeHeaders0pX(r *buff.Reader) header.Header0pX { n := int(r.PopUint16()) - headers := make(header.Header, n) + headers := make(header.Header0pX, n) for i := 0; i < n; i++ { key := r.PopUint16() val := r.PopBytes() @@ -44,7 +46,7 @@ func decodeHeaders(r *buff.Reader) header.Header { return headers } -func discardHeaders(r *buff.Reader) { +func discardHeaders0pX(r *buff.Reader) { n := int(r.PopUint16()) for i := 0; i < n; i++ { @@ -53,7 +55,41 @@ func discardHeaders(r *buff.Reader) { } } -func writeHeaders(w *buff.Writer, headers header.Header) { +func decodeHeaders1pX( + r *buff.Reader, + warningHandler WarningHandler, +) (header.Header1pX, error) { + n := int(r.PopUint16()) + + headers := make(header.Header1pX, n) + for i := 0; i < n; i++ { + headers[r.PopString()] = r.PopString() + } + + if data, ok := headers["warnings"]; ok { + var warnings []Warning + err := json.Unmarshal([]byte(data), &warnings) + if err != nil { + return nil, err + } + + errors := make([]error, len(warnings)) + for i, warning := range warnings { + errors[i] = errorFromCode(warning.Code, warning.Message) + } + + err = warningHandler(errors) + if err != nil { + return nil, err + } + } + + return headers, nil +} + +var decodeHeaders2pX = decodeHeaders1pX + +func writeHeaders0pX(w *buff.Writer, headers header.Header0pX) { w.PushUint16(uint16(len(headers))) for key, val := range headers { @@ -70,7 +106,7 @@ func (c *protocolConnection) execScriptFlow(r *buff.Reader, q *query) error { w := buff.NewWriter(c.writeMemory[:0]) w.BeginMessage(uint8(ExecuteScript)) - writeHeaders(w, q.headers0pX()) + writeHeaders0pX(w, q.headers0pX()) w.PushString(q.cmd) w.EndMessage() diff --git a/internal/client/transactable.go b/internal/client/transactable.go index db1cc0f..941d315 100644 --- a/internal/client/transactable.go +++ b/internal/client/transactable.go @@ -77,6 +77,7 @@ func (c *transactableConn) tx( ctx context.Context, action TxBlock, state map[string]interface{}, + warningHandler WarningHandler, ) (err error) { conn, err := c.borrow("transaction") if err != nil { @@ -101,6 +102,7 @@ func (c *transactableConn) tx( txState: &txState{}, options: c.txOpts, state: state, + warningHandler: warningHandler, } err = tx.start(ctx) if err != nil { diff --git a/internal/client/transaction.go b/internal/client/transaction.go index f013291..b5cda94 100644 --- a/internal/client/transaction.go +++ b/internal/client/transaction.go @@ -76,8 +76,9 @@ func (s *txState) assertStarted(opName string) error { type Tx struct { borrowableConn *txState - options TxOptions - state map[string]interface{} + options TxOptions + state map[string]interface{} + warningHandler WarningHandler } func (t *Tx) execute( @@ -93,6 +94,7 @@ func (t *Tx) execute( t.state, nil, false, + t.warningHandler, ) if err != nil { return err @@ -171,6 +173,7 @@ func (t *Tx) Execute( t.state, nil, true, + t.warningHandler, ) if err != nil { return err @@ -186,7 +189,16 @@ func (t *Tx) Query( out interface{}, args ...interface{}, ) error { - return runQuery(ctx, t, "Query", cmd, out, args, t.state) + return runQuery( + ctx, + t, + "Query", + cmd, + out, + args, + t.state, + t.warningHandler, + ) } // QuerySingle runs a singleton-returning query and returns its element. @@ -199,7 +211,16 @@ func (t *Tx) QuerySingle( out interface{}, args ...interface{}, ) error { - return runQuery(ctx, t, "QuerySingle", cmd, out, args, t.state) + return runQuery( + ctx, + t, + "QuerySingle", + cmd, + out, + args, + t.state, + t.warningHandler, + ) } // QueryJSON runs a query and return the results as JSON. @@ -209,7 +230,16 @@ func (t *Tx) QueryJSON( out *[]byte, args ...interface{}, ) error { - return runQuery(ctx, t, "QueryJSON", cmd, out, args, t.state) + return runQuery( + ctx, + t, + "QueryJSON", + cmd, + out, + args, + t.state, + t.warningHandler, + ) } // QuerySingleJSON runs a singleton-returning query. @@ -221,5 +251,14 @@ func (t *Tx) QuerySingleJSON( out interface{}, args ...interface{}, ) error { - return runQuery(ctx, t, "QuerySingleJSON", cmd, out, args, t.state) + return runQuery( + ctx, + t, + "QuerySingleJSON", + cmd, + out, + args, + t.state, + t.warningHandler, + ) } diff --git a/internal/client/warning.go b/internal/client/warning.go new file mode 100644 index 0000000..eb3c8f4 --- /dev/null +++ b/internal/client/warning.go @@ -0,0 +1,43 @@ +// This source file is part of the EdgeDB open source project. +// +// Copyright EdgeDB Inc. and the EdgeDB authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package edgedb + +import ( + "errors" + "log" +) + +// Warning is used to decode warnings in the protocol. +type Warning struct { + Code uint32 `json:"code"` + Message string `json:"message"` +} + +// LogWarnings is an edgedb.WarningHandler that logs warnings. +func LogWarnings(errors []error) error { + for _, err := range errors { + log.Println("EdgeDB warning:", err.Error()) + } + + return nil +} + +// WarningsAsErrors is an edgedb.WarningHandler that returns warnings as +// errors. +func WarningsAsErrors(warnings []error) error { + return errors.Join(warnings...) +} diff --git a/internal/cmd/export/names.txt b/internal/cmd/export/names.txt index b9b50ad..a24fea2 100644 --- a/internal/cmd/export/names.txt +++ b/internal/cmd/export/names.txt @@ -12,6 +12,7 @@ IsolationLevel LocalDate LocalDateTime LocalTime +LogWarnings Memory ModuleAlias NetworkError @@ -107,3 +108,5 @@ TxBlock TxConflict TxOptions UUID +WarningHandler +WarningsAsErrors diff --git a/internal/header/header.go b/internal/header/header.go index 1f4d33b..00df8b7 100644 --- a/internal/header/header.go +++ b/internal/header/header.go @@ -18,8 +18,11 @@ package header import "encoding/binary" -// Header is a binary protocol header -type Header map[uint16][]byte +// Header0pX is a binary protocol header for protocol major version 0 +type Header0pX map[uint16][]byte + +// Header1pX is a binary protocol header for protocol major version 1 +type Header1pX map[string]string const ( // AllowCapabilities tells the server what capabilities it should allow. diff --git a/rstdocs/api.rst b/rstdocs/api.rst index 91f71b9..2f7a815 100644 --- a/rstdocs/api.rst +++ b/rstdocs/api.rst @@ -195,4 +195,17 @@ TxOptions configures how transactions behave. .. code-block:: go - type TxOptions = edgedb.TxOptions \ No newline at end of file + type TxOptions = edgedb.TxOptions + + +*type* WarningHandler +--------------------- + +WarningHandler takes a slice of edgedb.Error that represent warnings and +optionally returns an error. This can be used to log warnings, increment +metrics, promote warnings to errors by returning them etc. + + +.. code-block:: go + + type WarningHandler = edgedb.WarningHandler \ No newline at end of file