Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notices #1439

Merged
merged 3 commits into from
Mar 4, 2025
Merged

Notices #1439

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions app/shared/display/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"strings"

"github.com/kwilteam/kwil-db/core/types"
)
Expand Down Expand Up @@ -33,15 +34,7 @@ func (h *TxHashAndExecResponse) UnmarshalJSON(b []byte) error {
// the JSON marshalling that is meant to be a composition of both RespTxHash and
// RespTxQuery.
func (h TxHashAndExecResponse) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf(`TxHash: %s
Status: %s
Height: %d
Log: %s`, hex.EncodeToString(h.Res.Hash[:]),
heightStatus(h.Res),
h.Res.Height,
h.Res.Result.Log,
),
), nil
return []byte(formatTxQueryResponseToText(h.Res)), nil
}

var _ MsgFormatter = (*TxHashAndExecResponse)(nil)
Expand Down Expand Up @@ -158,18 +151,7 @@ func heightStatus(res *types.TxQueryResponse) string {
}

func (r *RespTxQuery) MarshalText() ([]byte, error) {
msg := fmt.Sprintf(`Transaction ID: %s
Status: %s
Height: %d`,
r.Msg.Hash.String(),
heightStatus(r.Msg),
r.Msg.Height,
)

// result can be nil if it is still pending
if r.Msg.Result != nil {
msg += fmt.Sprintf("\nLog: %s", r.Msg.Result.Log)
}
msg := formatTxQueryResponseToText(r.Msg)

// Always try to serialize to verify hash, but only show raw if requested.
if r.Msg.Tx == nil {
Expand All @@ -189,3 +171,21 @@ Height: %d`,

return []byte(msg), nil
}

func formatTxQueryResponseToText(r *types.TxQueryResponse) string {
msg := fmt.Sprintf(`Transaction ID: %s
Status: %s
Height: %d`,
r.Hash.String(),
heightStatus(r),
r.Height,
)

// result can be nil if it is still pending
if r.Result != nil && r.Result.Log != "" {
msg += "\nLogs:"
msg += "\n " + strings.ReplaceAll(r.Result.Log, "\n", "\n ")
}

return msg
}
10 changes: 5 additions & 5 deletions app/shared/display/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func Test_TxHashAndExecResponse(t *testing.T) {
Res: qr,
}
expectJSON := `{"tx_hash":"0102030405000000000000000000000000000000000000000000000000000000","height":10,"tx":{"signature":{"sig":"yz/tf2/zblkFTASoMbIV5RQFJ1PuNT5v4x1LTvc2rNYVUSfbVV0wBroU/LTHm7rVbI5juBqYljGbsFOp4lNHWAA=","type":"secp256k1_ep"},"body":{"desc":"This is a test transaction for cli","payload":"AAA5AAAAeGY2MTdhZjFjYTc3NGViYmQ2ZDIzZThmZTEyYzU2ZDQxZDI1YTIyZDgxZTg4ZjY3YzZjNmVlMGQ0CwAAAGNyZWF0ZV91c2VyAQABAB4AAAAAAA8AAAAAAAAAAAR0ZXh0AAAAAAABAAMAAABmb28=","type":"execute","fee":"100","nonce":10,"chain_id":"asdf"},"serialization":"concat","sender":null},"tx_result":{"code":0,"gas":10,"log":"This is log","events":null}}`
expectText := "TxHash: 0102030405000000000000000000000000000000000000000000000000000000\nStatus: success\nHeight: 10\nLog: This is log"
expectText := "Transaction ID: 0102030405000000000000000000000000000000000000000000000000000000\nStatus: success\nHeight: 10\nLogs:\n This is log"

outText, err := resp.MarshalText()
assert.NoError(t, err, "MarshalText should not return error")
Expand Down Expand Up @@ -251,7 +251,7 @@ func TestRespTxQuery_MarshalText(t *testing.T) {
},
},
},
expected: "Transaction ID: 0100000000000000000000000000000000000000000000000000000000000000\nStatus: success\nHeight: 100\nLog: transaction successful",
expected: "Transaction ID: 0100000000000000000000000000000000000000000000000000000000000000\nStatus: success\nHeight: 100\nLogs:\n transaction successful",
},
{
name: "failed status",
Expand All @@ -265,7 +265,7 @@ func TestRespTxQuery_MarshalText(t *testing.T) {
},
},
},
expected: "Transaction ID: 0200000000000000000000000000000000000000000000000000000000000000\nStatus: failed\nHeight: 50\nLog: transaction failed",
expected: "Transaction ID: 0200000000000000000000000000000000000000000000000000000000000000\nStatus: failed\nHeight: 50\nLogs:\n transaction failed",
},
{
name: "pending status",
Expand All @@ -279,7 +279,7 @@ func TestRespTxQuery_MarshalText(t *testing.T) {
},
},
},
expected: "Transaction ID: 0300000000000000000000000000000000000000000000000000000000000000\nStatus: pending\nHeight: -1\nLog: transaction pending",
expected: "Transaction ID: 0300000000000000000000000000000000000000000000000000000000000000\nStatus: pending\nHeight: -1\nLogs:\n transaction pending",
},
{
name: "pending status",
Expand All @@ -303,7 +303,7 @@ func TestRespTxQuery_MarshalText(t *testing.T) {
},
},
},
expected: "Transaction ID: 1f456bec9c3819f077a7aafce25cf43ad9ab0a264cbae6efeaa8b92ec0bf4b47\nStatus: pending\nHeight: -1\nLog: transaction pending",
expected: "Transaction ID: 1f456bec9c3819f077a7aafce25cf43ad9ab0a264cbae6efeaa8b92ec0bf4b47\nStatus: pending\nHeight: -1\nLogs:\n transaction pending",
},
}

Expand Down
21 changes: 12 additions & 9 deletions cmd/kwil-cli/cmds/call-action.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/kwilteam/kwil-db/app/shared/display"
"github.com/kwilteam/kwil-db/cmd/kwil-cli/client"
Expand Down Expand Up @@ -176,21 +177,23 @@ func getStringRows(v [][]any) [][]string {
}

func (r *respCall) MarshalText() (text []byte, err error) {
if !r.PrintLogs {
return display.FormatTable(r.cmd, r.Data.QueryResult.ColumnNames, getStringRows(r.Data.QueryResult.Values))
}

bts, err := display.FormatTable(r.cmd, r.Data.QueryResult.ColumnNames, getStringRows(r.Data.QueryResult.Values))
if err != nil {
return nil, err
}

str := string(bts)
if r.Data.Error != nil {
str += "\n\nError: " + *r.Data.Error
}

if !r.PrintLogs {
return []byte(str), nil
}

if len(r.Data.Logs) > 0 {
bts = append(bts, []byte("\n\nLogs:")...)
for _, log := range r.Data.Logs {
bts = append(bts, []byte("\n "+log)...)
}
str += "\nLogs:\n " + strings.ReplaceAll(r.Data.Logs, "\n", "\n ")
}

return bts, nil
return []byte(str), nil
}
4 changes: 1 addition & 3 deletions cmd/kwil-cli/cmds/database/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,7 @@ func (r *respCall) MarshalText() (text []byte, err error) {

if len(r.Data.Logs) > 0 {
bts = append(bts, []byte("\n\nLogs:")...)
for _, log := range r.Data.Logs {
bts = append(bts, []byte("\n "+log)...)
}
bts = append(bts, []byte(r.Data.Logs)...)
}

return bts, nil
Expand Down
22 changes: 22 additions & 0 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/big"
"strconv"

"github.com/kwilteam/kwil-db/config"
"github.com/kwilteam/kwil-db/core/crypto"
Expand Down Expand Up @@ -149,6 +150,27 @@ type Engine interface {
type CallResult struct {
// Logs are the logs generated by the action.
Logs []string
// Error is an error that is raised during code execution.
// It is explicitly used for user-defined exceptions thrown
// with the `error` function.
Error error // TODO: implement
}

// FormatLogs formats the logs into a string.
func (c *CallResult) FormatLogs() string {
i := 0
var str string
for _, l := range c.Logs {
if i > 0 {
str += "\n"
}
// increment before formatting so that the first log is 1
i++
str += strconv.Itoa(i) + ". " + l

}

return str
}

// Row contains information about a row in a table.
Expand Down
3 changes: 2 additions & 1 deletion core/types/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ func (e *Event) UnmarshalBinary(data []byte) error {
// CallResult is the result of an action call.
type CallResult struct {
QueryResult *QueryResult `json:"query_result"`
Logs []string `json:"logs"`
Logs string `json:"logs"`
Error *string `json:"error"`
}

// QueryResult is the result of a SQL query or action.
Expand Down
2 changes: 1 addition & 1 deletion extensions/consensus/payloads.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ type Route interface {
// InTx executes the transaction, which may include state changes via the DB
// or Engine. The TxCode is returned by the Router, and it should be CodeOk
// for a nil error.
InTx(ctx *common.TxContext, app *common.App, tx *types.Transaction) (types.TxCode, error)
InTx(ctx *common.TxContext, app *common.App, tx *types.Transaction) (types.TxCode, string, error)
}
17 changes: 14 additions & 3 deletions node/block_processor/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"maps"
"math/big"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -396,6 +397,7 @@ func (bp *BlockProcessor) ExecuteBlock(ctx context.Context, req *ktypes.BlockExe
txResult := ktypes.TxResult{
Code: uint32(res.ResponseCode),
Gas: res.Spend,
Log: res.Log,
}

// bookkeeping for the block execution status
Expand All @@ -406,10 +408,19 @@ func (bp *BlockProcessor) ExecuteBlock(ctx context.Context, req *ktypes.BlockExe
return nil, fmt.Errorf("fatal db error during block execution: %w", res.Error)
}

txResult.Log = res.Error.Error()
if txResult.Log != "" {
txResult.Log += "\n"
}

// accounts for Postgres sometimes including
// an ERROR: prefix
resErr := res.Error.Error()
if !strings.HasPrefix(resErr, "ERROR: ") {
resErr = "ERROR: " + resErr
}

txResult.Log += resErr
bp.log.Info("Failed to execute transaction", "tx", txHash, "err", res.Error)
} else {
txResult.Log = "success"
}

txResults[i] = txResult
Expand Down
17 changes: 12 additions & 5 deletions node/engine/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,18 @@ var (
return nil, wrapErrArgumentType(types.TextType, args[0])
}

// technically error returns nothing, but for backwards compatibility with SELECT CASE we return null.
// It doesn't really matter, since error will cancel execution anyways.
return types.NullType, nil
// technically error returns nothing, but to allow it to be used in the query planner
// we have it return text. It doesn't really matter because it will cancel action execution.
return types.TextType, nil
},
PGFormatFunc: func(inputs []string) (string, error) {
def, err := defaultFormat("error")(inputs)
if err != nil {
return "", err
}

return def + "::text", nil
},
PGFormatFunc: defaultFormat("error"),
},
"parse_unix_timestamp": &ScalarFunctionDefinition{
ValidateArgsFunc: func(args []*types.DataType) (*types.DataType, error) {
Expand Down Expand Up @@ -92,7 +99,7 @@ var (
return types.NullType, nil
},
PGFormatFunc: func(inputs []string) (string, error) {
return "", fmt.Errorf("%w: notice cannot be used in SQL statements", ErrIllegalFunctionUsage)
return "", fmt.Errorf(`%w: "notice" cannot be used in SQL statements`, ErrIllegalFunctionUsage)
},
},
"uuid_generate_v5": &ScalarFunctionDefinition{
Expand Down
62 changes: 57 additions & 5 deletions node/engine/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package interpreter
import (
"context"
_ "embed"
"errors"
"fmt"
"maps"
"regexp"
Expand Down Expand Up @@ -378,6 +379,46 @@ func NewInterpreter(ctx context.Context, db sql.DB, service *common.Service, acc
return threadSafe, nil
}

// newUserDefinedErr makes an error that was returned from user-defined code using the ERROR function.
func newUserDefinedErr(e error) error {
return &userDefinedErr{err: e}
}

type userDefinedErr struct {
err error
}

func (u *userDefinedErr) Error() string {
return u.err.Error()
}

// unwrapExecutionErr unwraps an error that was returned from user-defined code using the ERROR function, or an error
// that is the result of user logic / data (e.g. a Postgres primary key violation).
// The error can either come from an action call to ERROR() or from Kwil's custom ERROR() postgres function.
// It returns the error, and whether it was a user logic error or something more serious.
// If it is a user-defined error, it will be unwrapped and returned as the error.
func unwrapExecutionErr(e error) (err error, isUserLogicErr bool) {
if e == nil {
return nil, false
}
as := new(userDefinedErr)
if errors.As(e, &as) {
return e, true
}

// if it is a SQL error, it might be a basic integrity constraint violation,
// which we should leave as-is but mark as a user logic error.
if allowedSQLSTATEErrRegex.MatchString(e.Error()) {
return e, true
}

return e, false
}

// checks for 22xxx and 23xxx SQLSTATE errors, or P0001 (raise_exception)
// https://www.postgresql.org/docs/current/errcodes-appendix.html
var allowedSQLSTATEErrRegex = regexp.MustCompile(`\(SQLSTATE ((23|22)\d{3}\)|P0001)`)

// funcDefToExecutable converts a Postgres function definition to an executable.
// This allows built-in Postgres functions to be used within the interpreter.
// This inconveniently requires a roundtrip to the database, but it is necessary
Expand Down Expand Up @@ -407,7 +448,7 @@ func funcDefToExecutable(funcName string, funcDef *engine.ScalarFunctionDefiniti
}

// if the function name is notice, then we need to get write the notice to our logs locally,
// instead of executing a query. This is the functional eqauivalent of Kwil's console.log().
// instead of executing a query. This is the functional equivalent of Kwil's console.log().
if funcName == "notice" {
var log string
if !args[0].Null() {
Expand All @@ -422,7 +463,7 @@ func funcDefToExecutable(funcName string, funcDef *engine.ScalarFunctionDefiniti
if !args[0].Null() {
msg = args[0].RawValue().(string)
}
return fmt.Errorf("error raised while executing: %s", msg)
return newUserDefinedErr(errors.New(msg))
}

zeroVal, err := newZeroValue(retTyp)
Expand Down Expand Up @@ -633,6 +674,9 @@ func (i *baseInterpreter) call(ctx *common.EngineContext, db sql.DB, namespace,
err = fmt.Errorf("panic: %v", r)
noErrOrPanic = false
}
if callRes != nil && callRes.Error != nil {
noErrOrPanic = false
}

if noErrOrPanic {
i.syncNamespaceManager()
Expand Down Expand Up @@ -715,13 +759,21 @@ func (i *baseInterpreter) call(ctx *common.EngineContext, db sql.DB, namespace,
err = exec.Func(execCtx, argVals, func(row *row) error {
return resultFn(rowToCommonRow(row))
})
if err != nil {
return nil, err

// if the error is an execution error,
// then it should be part of the CallResult,
// and not returned as a top-level error.
err, ok = unwrapExecutionErr(err)
if ok {
return &common.CallResult{
Logs: *execCtx.logs,
Error: err,
}, nil
}

return &common.CallResult{
Logs: *execCtx.logs,
}, nil
}, err
}

// rowToCommonRow converts a row to a common.Row.
Expand Down
Loading
Loading