diff --git a/.gitignore b/.gitignore index 2f8dc2a9..9e763227 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,9 @@ profile.* # Builds cmd/genji/genji -# VS Code config +# IDE config .vscode/ +.idea/ # gopls log gopls.log diff --git a/cmd/genji/doc/functions.go b/cmd/genji/doc/functions.go index 23c49aec..2115014f 100644 --- a/cmd/genji/doc/functions.go +++ b/cmd/genji/doc/functions.go @@ -4,19 +4,20 @@ type functionDocs map[string]string var packageDocs = map[string]functionDocs{ "strings": stringsDocs, - "math": mathDocs, - "": builtinDocs, + "math": mathDocs, + "": builtinDocs, } var builtinDocs = functionDocs{ - "pk": "The pk() function returns the primary key for the current document", - "count": "Returns a count of the number of times that arg1 is not NULL in a group. The count(*) function (with no arguments) returns the total number of rows in the group.", - "min": "Returns the minimum value of the arg1 expression in a group.", - "max": "Returns the maximum value of the arg1 expressein in a group.", - "sum": "The sum function returns the sum of all values taken by the arg1 expression in a group.", - "avg": "The avg function returns the average of all values taken by the arg1 expression in a group.", - "typeof": "The typeof function returns the type of arg1.", - "len": "The len function returns length of the arg1 expression if arg1 evals to string, array or document, either returns NULL.", + "pk": "The pk() function returns the primary key for the current document", + "count": "Returns a count of the number of times that arg1 is not NULL in a group. The count(*) function (with no arguments) returns the total number of rows in the group.", + "min": "Returns the minimum value of the arg1 expression in a group.", + "max": "Returns the maximum value of the arg1 expressein in a group.", + "sum": "The sum function returns the sum of all values taken by the arg1 expression in a group.", + "avg": "The avg function returns the average of all values taken by the arg1 expression in a group.", + "typeof": "The typeof function returns the type of arg1.", + "len": "The len function returns length of the arg1 expression if arg1 evals to string, array or document, either returns NULL.", + "coalesce": "The coalesce function returns the first non-null argument. NULL is returned if all arguments are null.", } var mathDocs = functionDocs{ @@ -31,9 +32,9 @@ var mathDocs = functionDocs{ } var stringsDocs = functionDocs{ - "lower": "The lower function returns arg1 to lower-case if arg1 evals to string", - "upper": "The upper function returns arg1 to upper-case if arg1 evals to string", - "trim": "The trim function returns arg1 with leading and trailing characters removed. space by default or arg2", - "ltrim": "The ltrim function returns arg1 with leading characters removed. space by default or arg2", - "rtrim": "The rtrim function returns arg1 with trailing characters removed. space by default or arg2", -} \ No newline at end of file + "lower": "The lower function returns arg1 to lower-case if arg1 evals to string", + "upper": "The upper function returns arg1 to upper-case if arg1 evals to string", + "trim": "The trim function returns arg1 with leading and trailing characters removed. space by default or arg2", + "ltrim": "The ltrim function returns arg1 with leading characters removed. space by default or arg2", + "rtrim": "The rtrim function returns arg1 with trailing characters removed. space by default or arg2", +} diff --git a/internal/expr/functions/builtins.go b/internal/expr/functions/builtins.go index 3cfad0e4..b7fb8921 100644 --- a/internal/expr/functions/builtins.go +++ b/internal/expr/functions/builtins.go @@ -67,6 +67,13 @@ var builtinFunctions = Definitions{ return &Len{Expr: args[0]}, nil }, }, + "coalesce": &definition{ + name: "coalesce", + arity: variadicArity, + constructorFn: func(args ...expr.Expr) (expr.Function, error) { + return &Coalesce{Exprs: args}, nil + }, + }, } // BuiltinDefinitions returns a map of builtin functions. @@ -716,4 +723,29 @@ func (s *Len) Params() []expr.Expr { return []expr.Expr{s.Expr} } // String returns the literal representation of len. func (s *Len) String() string { return fmt.Sprintf("LEN(%v)", s.Expr) -} \ No newline at end of file +} + +type Coalesce struct { + Exprs []expr.Expr +} + +func (c *Coalesce) Eval(e *environment.Environment) (types.Value, error) { + for _, exp := range c.Exprs { + v, err := exp.Eval(e) + if err != nil { + return nil, err + } + if v.Type() != types.NullValue { + return v, nil + } + } + return nil, nil +} + +func (c *Coalesce) String() string { + return fmt.Sprintf("COALESCE(%v)", c.Exprs) +} + +func (c *Coalesce) Params() []expr.Expr { + return c.Exprs +} diff --git a/internal/expr/functions/definition.go b/internal/expr/functions/definition.go index 45c2232c..6abba956 100644 --- a/internal/expr/functions/definition.go +++ b/internal/expr/functions/definition.go @@ -7,6 +7,9 @@ import ( "github.com/genjidb/genji/internal/expr" ) +// variadicArity represents an unlimited number of arguments. +const variadicArity = -1 + // A Definition transforms a list of expressions into a Function. type Definition interface { Name() string @@ -57,11 +60,10 @@ func (fd *definition) Name() string { } func (fd *definition) Function(args ...expr.Expr) (expr.Function, error) { - if fd.arity == -1 { - return fd.constructorFn(args...) + if fd.arity == variadicArity && len(args) == 0 { + return nil, fmt.Errorf("%s() requires at least one argument", fd.name) } - - if len(args) != fd.arity { + if fd.arity != variadicArity && (len(args) != fd.arity) { return nil, fmt.Errorf("%s() takes %d argument(s), not %d", fd.name, fd.arity, len(args)) } return fd.constructorFn(args...) diff --git a/internal/expr/functions/strings.go b/internal/expr/functions/strings.go index 6074a8af..b309ecf1 100644 --- a/internal/expr/functions/strings.go +++ b/internal/expr/functions/strings.go @@ -26,21 +26,21 @@ var stringsFunctions = Definitions{ }, "trim": &definition{ name: "trim", - arity: -1, + arity: variadicArity, constructorFn: func(args ...expr.Expr) (expr.Function, error) { return &Trim{Expr: args, TrimFunc: strings.Trim, Name: "TRIM"}, nil }, }, "ltrim": &definition{ name: "ltrim", - arity: -1, + arity: variadicArity, constructorFn: func(args ...expr.Expr) (expr.Function, error) { return &Trim{Expr: args, TrimFunc: strings.TrimLeft, Name: "LTRIM"}, nil }, }, "rtrim": &definition{ name: "rtrim", - arity: -1, + arity: variadicArity, constructorFn: func(args ...expr.Expr) (expr.Function, error) { return &Trim{Expr: args, TrimFunc: strings.TrimRight, Name: "RTRIM"}, nil }, diff --git a/internal/stream/operator.go b/internal/stream/operator.go index 5d623626..99c690e0 100644 --- a/internal/stream/operator.go +++ b/internal/stream/operator.go @@ -11,7 +11,7 @@ var ErrInvalidResult = errors.New("expression must evaluate to a document") // An Operator is used to modify a stream. // It takes an environment containing the current value as well as any other metadata -// created by other operatorsand returns a new environment which will be passed to the next operator. +// created by other operators and returns a new environment which will be passed to the next operator. // If it returns a nil environment, the env will be ignored. // If it returns an error, the stream will be interrupted and that error will bubble up // and returned by this function, unless that error is ErrStreamClosed, in which case diff --git a/sqltests/expr/coalesce.sql b/sqltests/expr/coalesce.sql new file mode 100644 index 00000000..44d0114b --- /dev/null +++ b/sqltests/expr/coalesce.sql @@ -0,0 +1,19 @@ +-- test: simple case +> COALESCE(1,2,3) +1 + +-- test: with null +> COALESCE(null,2,3) +2 + +-- test: with different values type +> COALESCE('hey',2,3) +'hey' + +-- test: with more than one null value with integer +> COALESCE(null, null, null,3) +3 + +-- test: with more than one null value with text +> COALESCE(null, null, null, 'hey') +'hey'