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

Add coalesce function #513

Merged
merged 9 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ profile.*
# Builds
cmd/genji/genji

# VS Code config
# IDE config
.vscode/
.idea/

# gopls log
gopls.log
Expand Down
33 changes: 17 additions & 16 deletions cmd/genji/doc/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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",
}
"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",
}
34 changes: 33 additions & 1 deletion internal/expr/functions/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@
return &Len{Expr: args[0]}, nil
},
},
"coalesce": &definition{
name: "coalesce",
arity: UNLIMITED,
constructorFn: func(args ...expr.Expr) (expr.Function, error) {
return &Coalesce{Exprs: args}, nil
},
},
}

// BuiltinDefinitions returns a map of builtin functions.
Expand Down Expand Up @@ -716,4 +723,29 @@
// String returns the literal representation of len.
func (s *Len) String() string {
return fmt.Sprintf("LEN(%v)", s.Expr)
}
}

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

Check warning on line 736 in internal/expr/functions/builtins.go

View check run for this annotation

Codecov / codecov/patch

internal/expr/functions/builtins.go#L736

Added line #L736 was not covered by tests
}
if v.Type() != types.NullValue {
return v, nil
}
}
return nil, nil

Check warning on line 742 in internal/expr/functions/builtins.go

View check run for this annotation

Codecov / codecov/patch

internal/expr/functions/builtins.go#L742

Added line #L742 was not covered by tests
}

func (c *Coalesce) String() string {
return fmt.Sprintf("COALESCE(%v)", c.Exprs)

Check warning on line 746 in internal/expr/functions/builtins.go

View check run for this annotation

Codecov / codecov/patch

internal/expr/functions/builtins.go#L745-L746

Added lines #L745 - L746 were not covered by tests
}

func (c *Coalesce) Params() []expr.Expr {
return c.Exprs

Check warning on line 750 in internal/expr/functions/builtins.go

View check run for this annotation

Codecov / codecov/patch

internal/expr/functions/builtins.go#L749-L750

Added lines #L749 - L750 were not covered by tests
}
10 changes: 6 additions & 4 deletions internal/expr/functions/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"github.com/genjidb/genji/internal/expr"
)

// UNLIMITED represents an unlimited number of arguments.
const UNLIMITED = -1
yaziine marked this conversation as resolved.
Show resolved Hide resolved

// A Definition transforms a list of expressions into a Function.
type Definition interface {
Name() string
Expand Down Expand Up @@ -57,11 +60,10 @@
}

func (fd *definition) Function(args ...expr.Expr) (expr.Function, error) {
if fd.arity == -1 {
return fd.constructorFn(args...)
if fd.arity == UNLIMITED && len(args) == 0 {
return nil, fmt.Errorf("%s() requires at least one argument", fd.name)

Check warning on line 64 in internal/expr/functions/definition.go

View check run for this annotation

Codecov / codecov/patch

internal/expr/functions/definition.go#L64

Added line #L64 was not covered by tests
}

if len(args) != fd.arity {
if fd.arity != UNLIMITED && (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...)
Expand Down
6 changes: 3 additions & 3 deletions internal/expr/functions/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,21 @@ var stringsFunctions = Definitions{
},
"trim": &definition{
name: "trim",
arity: -1,
arity: UNLIMITED,
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: UNLIMITED,
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: UNLIMITED,
constructorFn: func(args ...expr.Expr) (expr.Function, error) {
return &Trim{Expr: args, TrimFunc: strings.TrimRight, Name: "RTRIM"}, nil
},
Expand Down
3 changes: 3 additions & 0 deletions internal/sql/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@
// present, it unscans and return false. If the first is present, all the others
// must be parsed otherwise an error is returned.
func (p *Parser) parseOptional(tokens ...scanner.Token) (bool, error) {
if len(tokens) == 0 {
yaziine marked this conversation as resolved.
Show resolved Hide resolved
return false, nil

Check warning on line 272 in internal/sql/parser/parser.go

View check run for this annotation

Codecov / codecov/patch

internal/sql/parser/parser.go#L272

Added line #L272 was not covered by tests
}
// Parse optional first token
if tok, _, _ := p.ScanIgnoreWhitespace(); tok != tokens[0] {
p.Unscan()
Expand Down
2 changes: 1 addition & 1 deletion internal/stream/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions sqltests/expr/coalesce.sql
Original file line number Diff line number Diff line change
@@ -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'