From 5989efb2046da7a4480e19a343485fcdb02781d3 Mon Sep 17 00:00:00 2001 From: Dirkjan Bussink Date: Wed, 17 May 2023 11:34:46 +0200 Subject: [PATCH] evalengine: Add UUID functions (#13097) This adds `BIN_TO_UUID`, `IS_UUID`, `UUID` & `UUID_TO_BIN`. This doesn't implement `UUID_SHORT` though since that has specific requirements to use the `server_id` which is not something that conceptually makes sense for `vtgate` since it works across different backend MySQL servers. So we avoid adding that here since there's no clear path for how to implement that. Signed-off-by: Dirkjan Bussink --- go/vt/vtgate/evalengine/cached_size.go | 48 ++++ go/vt/vtgate/evalengine/compiler_asm.go | 118 +++++++++ go/vt/vtgate/evalengine/eval.go | 36 +++ go/vt/vtgate/evalengine/eval_temporal.go | 30 --- go/vt/vtgate/evalengine/fn_misc.go | 257 +++++++++++++++++++ go/vt/vtgate/evalengine/testcases/cases.go | 65 +++++ go/vt/vtgate/evalengine/testcases/inputs.go | 12 + go/vt/vtgate/evalengine/translate_builtin.go | 24 ++ 8 files changed, 560 insertions(+), 30 deletions(-) diff --git a/go/vt/vtgate/evalengine/cached_size.go b/go/vt/vtgate/evalengine/cached_size.go index 66a9c845dd6..22b0ac78acf 100644 --- a/go/vt/vtgate/evalengine/cached_size.go +++ b/go/vt/vtgate/evalengine/cached_size.go @@ -429,6 +429,18 @@ func (cached *builtinAtan2) CachedSize(alloc bool) int64 { size += cached.CallExpr.CachedSize(false) return size } +func (cached *builtinBinToUUID) CachedSize(alloc bool) int64 { + if cached == nil { + return int64(0) + } + size := int64(0) + if alloc { + size += int64(48) + } + // field CallExpr vitess.io/vitess/go/vt/vtgate/evalengine.CallExpr + size += cached.CallExpr.CachedSize(false) + return size +} func (cached *builtinBitCount) CachedSize(alloc bool) int64 { if cached == nil { return int64(0) @@ -861,6 +873,18 @@ func (cached *builtinIsIPV6) CachedSize(alloc bool) int64 { size += cached.CallExpr.CachedSize(false) return size } +func (cached *builtinIsUUID) CachedSize(alloc bool) int64 { + if cached == nil { + return int64(0) + } + size := int64(0) + if alloc { + size += int64(48) + } + // field CallExpr vitess.io/vitess/go/vt/vtgate/evalengine.CallExpr + size += cached.CallExpr.CachedSize(false) + return size +} func (cached *builtinJSONArray) CachedSize(alloc bool) int64 { if cached == nil { return int64(0) @@ -1401,6 +1425,30 @@ func (cached *builtinTruncate) CachedSize(alloc bool) int64 { size += cached.CallExpr.CachedSize(false) return size } +func (cached *builtinUUID) CachedSize(alloc bool) int64 { + if cached == nil { + return int64(0) + } + size := int64(0) + if alloc { + size += int64(48) + } + // field CallExpr vitess.io/vitess/go/vt/vtgate/evalengine.CallExpr + size += cached.CallExpr.CachedSize(false) + return size +} +func (cached *builtinUUIDToBin) CachedSize(alloc bool) int64 { + if cached == nil { + return int64(0) + } + size := int64(0) + if alloc { + size += int64(48) + } + // field CallExpr vitess.io/vitess/go/vt/vtgate/evalengine.CallExpr + size += cached.CallExpr.CachedSize(false) + return size +} func (cached *builtinUnhex) CachedSize(alloc bool) int64 { if cached == nil { return int64(0) diff --git a/go/vt/vtgate/evalengine/compiler_asm.go b/go/vt/vtgate/evalengine/compiler_asm.go index af0b90ee40e..8be4707977d 100644 --- a/go/vt/vtgate/evalengine/compiler_asm.go +++ b/go/vt/vtgate/evalengine/compiler_asm.go @@ -33,6 +33,8 @@ import ( "strconv" "time" + "github.com/google/uuid" + "vitess.io/vitess/go/hack" "vitess.io/vitess/go/mysql/collations" "vitess.io/vitess/go/mysql/collations/charset" @@ -4122,3 +4124,119 @@ func (asm *assembler) Fn_CONCAT_WS(tt querypb.Type, tc collations.TypedCollation return 1 }, "FN CONCAT_WS VARCHAR(SP-1) VARCHAR(SP-2)...VARCHAR(SP-N)") } + +func (asm *assembler) Fn_BIN_TO_UUID0(col collations.TypedCollation) { + asm.emit(func(env *ExpressionEnv) int { + arg := env.vm.stack[env.vm.sp-1].(*evalBytes) + + parsed, err := uuid.FromBytes(arg.bytes) + if err != nil { + env.vm.stack[env.vm.sp-1] = nil + env.vm.err = errIncorrectUUID(arg.bytes, "bin_to_uuid") + return 1 + } + arg.bytes = hack.StringBytes(parsed.String()) + arg.tt = int16(sqltypes.VarChar) + arg.col = col + return 1 + }, "FN BIN_TO_UUID VARBINARY(SP-1)") +} + +func (asm *assembler) Fn_BIN_TO_UUID1(col collations.TypedCollation) { + asm.adjustStack(-1) + asm.emit(func(env *ExpressionEnv) int { + arg := env.vm.stack[env.vm.sp-2].(*evalBytes) + b := arg.bytes + + if env.vm.stack[env.vm.sp-1] != nil && + env.vm.stack[env.vm.sp-1].(*evalInt64).i != 0 { + b = swapUUIDFrom(b) + } + + parsed, err := uuid.FromBytes(b) + if err != nil { + env.vm.stack[env.vm.sp-2] = nil + env.vm.err = errIncorrectUUID(arg.bytes, "bin_to_uuid") + env.vm.sp-- + return 1 + } + arg.bytes = hack.StringBytes(parsed.String()) + arg.tt = int16(sqltypes.VarChar) + arg.col = col + env.vm.sp-- + return 1 + }, "FN BIN_TO_UUID VARBINARY(SP-2) INT64(SP-1)") +} + +func (asm *assembler) Fn_IS_UUID() { + asm.emit(func(env *ExpressionEnv) int { + arg := env.vm.stack[env.vm.sp-1].(*evalBytes) + + _, err := uuid.ParseBytes(arg.bytes) + env.vm.stack[env.vm.sp-1] = env.vm.arena.newEvalBool(err == nil) + return 1 + }, "FN IS_UUID VARBINARY(SP-1)") +} + +func (asm *assembler) Fn_UUID() { + asm.adjustStack(1) + asm.emit(func(env *ExpressionEnv) int { + v, err := uuid.NewUUID() + if err != nil { + env.vm.err = err + env.vm.sp++ + return 1 + } + m, err := v.MarshalText() + if err != nil { + env.vm.err = err + env.vm.sp++ + return 1 + } + + env.vm.stack[env.vm.sp] = env.vm.arena.newEvalText(m, collationUtf8mb3) + env.vm.sp++ + return 1 + }, "FN UUID") +} + +func (asm *assembler) Fn_UUID_TO_BIN0() { + asm.emit(func(env *ExpressionEnv) int { + arg := env.vm.stack[env.vm.sp-1].(*evalBytes) + + parsed, err := uuid.ParseBytes(arg.bytes) + if err != nil { + env.vm.stack[env.vm.sp-1] = nil + env.vm.err = errIncorrectUUID(arg.bytes, "uuid_to_bin") + return 1 + } + arg.bytes = parsed[:] + arg.tt = int16(sqltypes.VarBinary) + arg.col = collationBinary + return 1 + }, "FN UUID_TO_BIN VARBINARY(SP-1)") +} + +func (asm *assembler) Fn_UUID_TO_BIN1() { + asm.adjustStack(-1) + asm.emit(func(env *ExpressionEnv) int { + arg := env.vm.stack[env.vm.sp-2].(*evalBytes) + parsed, err := uuid.ParseBytes(arg.bytes) + if err != nil { + env.vm.stack[env.vm.sp-2] = nil + env.vm.err = errIncorrectUUID(arg.bytes, "uuid_to_bin") + env.vm.sp-- + return 1 + } + b := parsed[:] + if env.vm.stack[env.vm.sp-1] != nil && + env.vm.stack[env.vm.sp-1].(*evalInt64).i != 0 { + b = swapUUIDTo(b) + } + arg.bytes = b + arg.tt = int16(sqltypes.VarBinary) + arg.col = collationBinary + env.vm.sp-- + return 1 + }, "FN UUID_TO_BIN VARBINARY(SP-2) INT64(SP-1)") +} diff --git a/go/vt/vtgate/evalengine/eval.go b/go/vt/vtgate/evalengine/eval.go index 6ca8b965340..d264ea52c1b 100644 --- a/go/vt/vtgate/evalengine/eval.go +++ b/go/vt/vtgate/evalengine/eval.go @@ -19,6 +19,7 @@ package evalengine import ( "fmt" "strconv" + "unicode/utf8" "vitess.io/vitess/go/hack" "vitess.io/vitess/go/mysql/collations" @@ -361,3 +362,38 @@ func valueToEval(value sqltypes.Value, collation collations.TypedCollation) (eva return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "Type is not supported: %q %s", value, value.Type()) } } + +const hexchars = "0123456789ABCDEF" + +func sanitizeErrorValue(s []byte) []byte { + var buf []byte + for width := 0; len(s) > 0; s = s[width:] { + r := rune(s[0]) + width = 1 + if r >= utf8.RuneSelf { + r, width = utf8.DecodeLastRune(s) + } + if width == 1 && r == utf8.RuneError { + buf = append(buf, `\x`...) + buf = append(buf, hexchars[s[0]>>4]) + buf = append(buf, hexchars[s[0]&0xF]) + continue + } + + if strconv.IsPrint(r) { + if r < utf8.RuneSelf { + buf = append(buf, byte(r)) + } else { + b := [utf8.UTFMax]byte{} + n := utf8.EncodeRune(b[:], r) + buf = append(buf, b[:n]...) + } + continue + } + + buf = append(buf, `\x`...) + buf = append(buf, hexchars[s[0]>>4]) + buf = append(buf, hexchars[s[0]&0xF]) + } + return buf +} diff --git a/go/vt/vtgate/evalengine/eval_temporal.go b/go/vt/vtgate/evalengine/eval_temporal.go index 1efd988b6e9..e04f6174dd6 100644 --- a/go/vt/vtgate/evalengine/eval_temporal.go +++ b/go/vt/vtgate/evalengine/eval_temporal.go @@ -2,7 +2,6 @@ package evalengine import ( "time" - "unicode/utf8" "vitess.io/vitess/go/hack" "vitess.io/vitess/go/mysql/datetime" @@ -156,35 +155,6 @@ func newEvalTime(time datetime.Time, l int) *evalTemporal { return &evalTemporal{t: sqltypes.Time, dt: datetime.DateTime{Time: time.Round(l)}, prec: uint8(l)} } -func sanitizeErrorValue(s []byte) []byte { - b := make([]byte, 0, len(s)+1) - invalid := false // previous byte was from an invalid UTF-8 sequence - for i := 0; i < len(s); { - c := s[i] - if c < utf8.RuneSelf { - i++ - invalid = false - if c != 0 { - b = append(b, c) - } - continue - } - _, wid := utf8.DecodeRune(s[i:]) - if wid == 1 { - i++ - if !invalid { - invalid = true - b = append(b, '?') - } - continue - } - invalid = false - b = append(b, s[i:i+wid]...) - i += wid - } - return b -} - func errIncorrectTemporal(date string, in []byte) error { return vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.WrongValue, "Incorrect %s value: '%s'", date, sanitizeErrorValue(in)) } diff --git a/go/vt/vtgate/evalengine/fn_misc.go b/go/vt/vtgate/evalengine/fn_misc.go index aabd746dac3..96522a2314f 100644 --- a/go/vt/vtgate/evalengine/fn_misc.go +++ b/go/vt/vtgate/evalengine/fn_misc.go @@ -21,10 +21,14 @@ import ( "math" "net/netip" + "github.com/google/uuid" + "vitess.io/vitess/go/hack" "vitess.io/vitess/go/mysql/collations" "vitess.io/vitess/go/sqltypes" querypb "vitess.io/vitess/go/vt/proto/query" + vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" + "vitess.io/vitess/go/vt/vterrors" ) type ( @@ -61,6 +65,23 @@ type ( builtinIsIPV6 struct { CallExpr } + + builtinBinToUUID struct { + CallExpr + collate collations.ID + } + + builtinIsUUID struct { + CallExpr + } + + builtinUUID struct { + CallExpr + } + + builtinUUIDToBin struct { + CallExpr + } ) var _ Expr = (*builtinInetAton)(nil) @@ -71,6 +92,10 @@ var _ Expr = (*builtinIsIPV4)(nil) var _ Expr = (*builtinIsIPV4Compat)(nil) var _ Expr = (*builtinIsIPV4Mapped)(nil) var _ Expr = (*builtinIsIPV6)(nil) +var _ Expr = (*builtinBinToUUID)(nil) +var _ Expr = (*builtinIsUUID)(nil) +var _ Expr = (*builtinUUID)(nil) +var _ Expr = (*builtinUUIDToBin)(nil) func (call *builtinInetAton) eval(env *ExpressionEnv) (eval, error) { arg, err := call.arg1(env) @@ -402,3 +427,235 @@ func (call *builtinIsIPV6) compile(c *compiler) (ctype, error) { return ctype{Type: sqltypes.Int64, Flag: arg.Flag | flagIsBoolean, Col: collationNumeric}, nil } + +func errIncorrectUUID(in []byte, f string) error { + return vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.WrongValue, "Incorrect string value: '%s' for function %s", sanitizeErrorValue(in), f) +} + +func swapUUIDFrom(in []byte) []byte { + if len(in) != 16 { + return in + } + out := make([]byte, 0, 16) + out = append(out, in[4:8]...) + out = append(out, in[2:4]...) + out = append(out, in[0:2]...) + out = append(out, in[8:]...) + return out +} + +func swapUUIDTo(in []byte) []byte { + if len(in) != 16 { + return in + } + + out := make([]byte, 0, 16) + out = append(out, in[6:8]...) + out = append(out, in[4:6]...) + out = append(out, in[0:4]...) + out = append(out, in[8:]...) + return out +} + +func (call *builtinBinToUUID) eval(env *ExpressionEnv) (eval, error) { + arg, err := call.arg1(env) + if arg == nil || err != nil { + return nil, err + } + + raw := evalToBinary(arg).bytes + + if len(call.Arguments) > 1 { + swap, err := call.Arguments[1].eval(env) + if err != nil { + return nil, err + } + + if swap != nil && evalToInt64(swap).i != 0 { + raw = swapUUIDFrom(raw) + } + } + + parsed, err := uuid.FromBytes(raw) + if err != nil { + return nil, errIncorrectUUID(raw, "bin_to_uuid") + } + return newEvalText(hack.StringBytes(parsed.String()), defaultCoercionCollation(call.collate)), nil +} + +func (call *builtinBinToUUID) typeof(env *ExpressionEnv, fields []*querypb.Field) (sqltypes.Type, typeFlag) { + _, f := call.Arguments[0].typeof(env, fields) + return sqltypes.VarChar, f +} + +func (call *builtinBinToUUID) compile(c *compiler) (ctype, error) { + arg, err := call.Arguments[0].compile(c) + if err != nil { + return ctype{}, err + } + skip := c.compileNullCheck1(arg) + + switch { + case arg.isTextual(): + default: + c.asm.Convert_xb(1, sqltypes.VarBinary, 0, false) + } + + col := defaultCoercionCollation(call.collate) + ct := ctype{Type: sqltypes.VarChar, Flag: arg.Flag, Col: col} + + if len(call.Arguments) == 1 { + c.asm.Fn_BIN_TO_UUID0(col) + c.asm.jumpDestination(skip) + return ct, nil + } + + swap, err := call.Arguments[1].compile(c) + if err != nil { + return ctype{}, err + } + + sj := c.compileNullCheck1(swap) + switch swap.Type { + case sqltypes.Int64: + case sqltypes.Uint64: + c.asm.Convert_ui(1) + default: + c.asm.Convert_xi(1) + } + c.asm.jumpDestination(sj) + c.asm.Fn_BIN_TO_UUID1(col) + + c.asm.jumpDestination(skip) + return ct, nil +} + +func (call *builtinIsUUID) eval(env *ExpressionEnv) (eval, error) { + arg, err := call.arg1(env) + if arg == nil || err != nil { + return nil, err + } + + raw := evalToBinary(arg).bytes + _, err = uuid.ParseBytes(raw) + return newEvalBool(err == nil), nil +} + +func (call *builtinIsUUID) typeof(env *ExpressionEnv, fields []*querypb.Field) (sqltypes.Type, typeFlag) { + _, f := call.Arguments[0].typeof(env, fields) + return sqltypes.Int64, f | flagIsBoolean +} + +func (call *builtinIsUUID) compile(c *compiler) (ctype, error) { + arg, err := call.Arguments[0].compile(c) + if err != nil { + return ctype{}, err + } + skip := c.compileNullCheck1(arg) + + switch { + case arg.isTextual(): + default: + c.asm.Convert_xb(1, sqltypes.VarBinary, 0, false) + } + c.asm.Fn_IS_UUID() + + c.asm.jumpDestination(skip) + return ctype{Type: sqltypes.Int64, Flag: arg.Flag | flagIsBoolean, Col: collationNumeric}, nil +} + +func (call *builtinUUID) eval(env *ExpressionEnv) (eval, error) { + v, err := uuid.NewUUID() + if err != nil { + return nil, err + } + m, err := v.MarshalText() + if err != nil { + return nil, err + } + + return newEvalText(m, collationUtf8mb3), nil +} + +func (call *builtinUUID) typeof(env *ExpressionEnv, fields []*querypb.Field) (sqltypes.Type, typeFlag) { + return sqltypes.VarChar, 0 +} + +func (call *builtinUUID) compile(c *compiler) (ctype, error) { + c.asm.Fn_UUID() + return ctype{Type: sqltypes.VarChar, Flag: 0, Col: collationUtf8mb3}, nil +} + +func (call *builtinUUIDToBin) eval(env *ExpressionEnv) (eval, error) { + arg, err := call.arg1(env) + if arg == nil || err != nil { + return nil, err + } + + raw := evalToBinary(arg).bytes + + parsed, err := uuid.ParseBytes(raw) + if err != nil { + return nil, errIncorrectUUID(raw, "uuid_to_bin") + } + + out := parsed[:] + if len(call.Arguments) > 1 { + swap, err := call.Arguments[1].eval(env) + if err != nil { + return nil, err + } + + if swap != nil && evalToInt64(swap).i != 0 { + out = swapUUIDTo(out) + } + } + + return newEvalBinary(out), nil +} + +func (call *builtinUUIDToBin) typeof(env *ExpressionEnv, fields []*querypb.Field) (sqltypes.Type, typeFlag) { + _, f := call.Arguments[0].typeof(env, fields) + return sqltypes.VarBinary, f +} + +func (call *builtinUUIDToBin) compile(c *compiler) (ctype, error) { + arg, err := call.Arguments[0].compile(c) + if err != nil { + return ctype{}, err + } + skip := c.compileNullCheck1(arg) + + switch { + case arg.isTextual(): + default: + c.asm.Convert_xb(1, sqltypes.VarBinary, 0, false) + } + + ct := ctype{Type: sqltypes.VarBinary, Flag: arg.Flag, Col: collationBinary} + + if len(call.Arguments) == 1 { + c.asm.Fn_UUID_TO_BIN0() + c.asm.jumpDestination(skip) + return ct, nil + } + + swap, err := call.Arguments[1].compile(c) + if err != nil { + return ctype{}, err + } + + sj := c.compileNullCheck1(swap) + switch swap.Type { + case sqltypes.Int64: + case sqltypes.Uint64: + c.asm.Convert_ui(1) + default: + c.asm.Convert_xi(1) + } + c.asm.jumpDestination(sj) + c.asm.Fn_UUID_TO_BIN1() + + c.asm.jumpDestination(skip) + return ct, nil +} diff --git a/go/vt/vtgate/evalengine/testcases/cases.go b/go/vt/vtgate/evalengine/testcases/cases.go index a2039404efc..cb03c6cb08f 100644 --- a/go/vt/vtgate/evalengine/testcases/cases.go +++ b/go/vt/vtgate/evalengine/testcases/cases.go @@ -146,6 +146,10 @@ var Cases = []TestCase{ {Run: FnIsIPv4Compat}, {Run: FnIsIPv4Mapped}, {Run: FnIsIPv6}, + {Run: FnBinToUUID}, + {Run: FnIsUUID}, + {Run: FnUUID}, + {Run: FnUUIDToBin}, } func JSONPathOperations(yield Query) { @@ -1782,3 +1786,64 @@ func FnIsIPv6(yield Query) { yield(fmt.Sprintf("IS_IPV6(%s)", d), nil) } } + +func FnBinToUUID(yield Query) { + args := []string{ + "NULL", + "-1", + "0", + "1", + "2", + "''", + "'-1'", + "'0'", + "'1'", + "'2'", + } + for _, d := range uuidInputs { + yield(fmt.Sprintf("BIN_TO_UUID(%s)", d), nil) + } + + for _, d := range uuidInputs { + for _, a := range args { + yield(fmt.Sprintf("BIN_TO_UUID(%s, %s)", d, a), nil) + } + } +} + +func FnIsUUID(yield Query) { + for _, d := range uuidInputs { + yield(fmt.Sprintf("IS_UUID(%s)", d), nil) + } +} + +func FnUUID(yield Query) { + yield("LENGTH(UUID())", nil) + yield("COLLATION(UUID())", nil) + yield("IS_UUID(UUID())", nil) + yield("LENGTH(UUID_TO_BIN(UUID())", nil) +} + +func FnUUIDToBin(yield Query) { + args := []string{ + "NULL", + "-1", + "0", + "1", + "2", + "''", + "'-1'", + "'0'", + "'1'", + "'2'", + } + for _, d := range uuidInputs { + yield(fmt.Sprintf("UUID_TO_BIN(%s)", d), nil) + } + + for _, d := range uuidInputs { + for _, a := range args { + yield(fmt.Sprintf("UUID_TO_BIN(%s, %s)", d, a), nil) + } + } +} diff --git a/go/vt/vtgate/evalengine/testcases/inputs.go b/go/vt/vtgate/evalengine/testcases/inputs.go index fe5e3c57f35..8d81a4b4122 100644 --- a/go/vt/vtgate/evalengine/testcases/inputs.go +++ b/go/vt/vtgate/evalengine/testcases/inputs.go @@ -273,3 +273,15 @@ var ipInputs = []string{ strconv.FormatUint(math.MaxUint32+1, 10), "0x0000000000000000000000000A000509", } + +var uuidInputs = []string{ + "NULL", + "'foobar'", + "''", + "'09db81f6-f266-11ed-a6f9-20fc8fd6830e'", + "'09db81f6f26611eda6f920fc8fd6830e'", + "'{09db81f6-f266-11ed-a6f9-20fc8fd6830e}'", + "0x0000000000000000000000000A000509", + "0x09DB81F6F26611EDA6F920FC8FD6830E", + "0x11EDF26609DB81F6A6F920FC8FD6830E", +} diff --git a/go/vt/vtgate/evalengine/translate_builtin.go b/go/vt/vtgate/evalengine/translate_builtin.go index c366ab45a0a..e5045a1900f 100644 --- a/go/vt/vtgate/evalengine/translate_builtin.go +++ b/go/vt/vtgate/evalengine/translate_builtin.go @@ -505,6 +505,30 @@ func (ast *astCompiler) translateFuncExpr(fn *sqlparser.FuncExpr) (Expr, error) return nil, argError(method) } return &builtinIsIPV6{CallExpr: call}, nil + case "bin_to_uuid": + switch len(args) { + case 1, 2: + return &builtinBinToUUID{CallExpr: call, collate: ast.cfg.Collation}, nil + default: + return nil, argError(method) + } + case "is_uuid": + if len(args) != 1 { + return nil, argError(method) + } + return &builtinIsUUID{CallExpr: call}, nil + case "uuid": + if len(args) != 0 { + return nil, argError(method) + } + return &builtinUUID{CallExpr: call}, nil + case "uuid_to_bin": + switch len(args) { + case 1, 2: + return &builtinUUIDToBin{CallExpr: call}, nil + default: + return nil, argError(method) + } case "user", "current_user", "session_user", "system_user": if len(args) != 0 { return nil, argError(method)