diff --git a/cmd/genji/doc/doc_test.go b/cmd/genji/doc/doc_test.go index 928969c0..db89d462 100644 --- a/cmd/genji/doc/doc_test.go +++ b/cmd/genji/doc/doc_test.go @@ -28,8 +28,10 @@ func TestFunctions(t *testing.T) { t.Run(fmt.Sprintf("%s.%s is documented and has all its arguments mentioned", pkgname, fname), func(t *testing.T) { str, err := doc.DocString(fmt.Sprintf("%s.%s", pkgname, fname)) assert.NoError(t, err) - for i := 0; i < def.Arity(); i++ { - require.Contains(t, trimDocPromt(str), fmt.Sprintf("arg%d", i+1)) + if def.Arity() > 0 { + for i := 0; i < def.Arity(); i++ { + require.Contains(t, trimDocPromt(str), fmt.Sprintf("arg%d", i+1)) + } } }) } diff --git a/cmd/genji/doc/functions.go b/cmd/genji/doc/functions.go index f81ff24c..23c49aec 100644 --- a/cmd/genji/doc/functions.go +++ b/cmd/genji/doc/functions.go @@ -33,4 +33,7 @@ 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 diff --git a/internal/expr/functions/definition.go b/internal/expr/functions/definition.go index 8f74094b..45c2232c 100644 --- a/internal/expr/functions/definition.go +++ b/internal/expr/functions/definition.go @@ -23,8 +23,8 @@ type Packages map[string]Definitions func DefaultPackages() Packages { return Packages{ - "": BuiltinDefinitions(), - "math": MathFunctions(), + "": BuiltinDefinitions(), + "math": MathFunctions(), "strings": StringsDefinitions(), } } @@ -57,6 +57,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 len(args) != fd.arity { return nil, fmt.Errorf("%s() takes %d argument(s), not %d", fd.name, fd.arity, len(args)) } @@ -64,8 +68,12 @@ func (fd *definition) Function(args ...expr.Expr) (expr.Function, error) { } func (fd *definition) String() string { - args := make([]string, 0, fd.arity) - for i := 0; i < fd.arity; i++ { + arity := fd.arity + if arity < 0 { + arity = 0 + } + args := make([]string, 0, arity) + for i := 0; i < arity; i++ { args = append(args, fmt.Sprintf("arg%d", i+1)) } return fmt.Sprintf("%s(%s)", fd.name, strings.Join(args, ", ")) diff --git a/internal/expr/functions/strings.go b/internal/expr/functions/strings.go index 264f20f8..6074a8af 100644 --- a/internal/expr/functions/strings.go +++ b/internal/expr/functions/strings.go @@ -24,6 +24,27 @@ var stringsFunctions = Definitions{ return &Upper{Expr: args[0]}, nil }, }, + "trim": &definition{ + name: "trim", + arity: -1, + constructorFn: func(args ...expr.Expr) (expr.Function, error) { + return &Trim{Expr: args, TrimFunc: strings.Trim, Name: "TRIM"}, nil + }, + }, + "ltrim": &definition{ + name: "ltrim", + arity: -1, + constructorFn: func(args ...expr.Expr) (expr.Function, error) { + return &Trim{Expr: args, TrimFunc: strings.TrimLeft, Name: "LTRIM"}, nil + }, + }, + "rtrim": &definition{ + name: "rtrim", + arity: -1, + constructorFn: func(args ...expr.Expr) (expr.Function, error) { + return &Trim{Expr: args, TrimFunc: strings.TrimRight, Name: "RTRIM"}, nil + }, + }, } func StringsDefinitions() Definitions { @@ -109,3 +130,79 @@ func (s *Upper) Params() []expr.Expr { return []expr.Expr{s.Expr} } func (s *Upper) String() string { return fmt.Sprintf("UPPER(%v)", s.Expr) } + +// TRIM removes leading and trailing characters from a string based on the given input. +// LTRIM removes leading characters +// RTRIM removes trailing characters +// By default remove space " " +type Trim struct { + Expr []expr.Expr + TrimFunc TrimFunc + Name string +} + +type TrimFunc func(string, string) string + +func (s *Trim) Eval(env *environment.Environment) (types.Value, error) { + if len(s.Expr) > 2 { + return nil, fmt.Errorf("misuse of string function %v()", s.Name) + } + + input, err := s.Expr[0].Eval(env) + if err != nil { + return nil, err + } + + if input.Type() != types.TextValue { + return types.NewNullValue(), nil + } + + var cutset = " " + + if len(s.Expr) == 2 { + remove, err := s.Expr[1].Eval(env) + if err != nil { + return nil, err + } + if remove.Type() != types.TextValue { + return types.NewNullValue(), nil + } + cutset = types.As[string](remove) + } + + trimmed := s.TrimFunc(types.As[string](input), cutset) + + return types.NewTextValue(trimmed), nil +} + +func (s *Trim) IsEqual(other expr.Expr) bool { + if other == nil { + return false + } + o, ok := other.(*Trim) + if !ok { + return false + } + if len(s.Expr) != len(o.Expr) { + return false + } + + for i := range s.Expr { + if !expr.Equal(s.Expr[i], o.Expr[i]) { + return false + } + } + + return true +} + +func (s *Trim) Params() []expr.Expr { + return s.Expr +} + +func (s *Trim) String() string { + if len(s.Expr) == 1 { + return fmt.Sprintf("%v(%v)", s.Name, s.Expr[0]) + } + return fmt.Sprintf("%v(%v, %v)", s.Name, s.Expr[0], s.Expr[1]) +} diff --git a/sqltests/SELECT/STRINGS/ltrim.sql b/sqltests/SELECT/STRINGS/ltrim.sql new file mode 100644 index 00000000..806bf5b2 --- /dev/null +++ b/sqltests/SELECT/STRINGS/ltrim.sql @@ -0,0 +1,112 @@ +-- setup: +CREATE TABLE test( + a TEXT +); + +INSERT INTO test (a) VALUES (" hello "), ("!hello!"), (" !hello! "); + +-- test: LTRIM TEXT default +SELECT strings.LTRIM(a) FROM test; +/* result: +{ + "LTRIM(a)": "hello " +} +{ + "LTRIM(a)": "!hello!" +} +{ + "LTRIM(a)": "!hello! " +} +*/ + + +-- test: LTRIM TEXT with param +SELECT strings.LTRIM(a, "!") FROM test; +/* result: +{ + "LTRIM(a, \"!\")": " hello " +} +{ + "LTRIM(a, \"!\")": "hello!" +} +{ + "LTRIM(a, \"!\")": " !hello! " +} +*/ + +-- test: LTRIM TEXT with multiple char params +SELECT strings.LTRIM(a, " !") FROM test; +/* result: +{ + "LTRIM(a, \" !\")": "hello " +} +{ + "LTRIM(a, \" !\")": "hello!" +} +{ + "LTRIM(a, \" !\")": "hello! " +} +*/ + + +-- test: LTRIM TEXT with multiple char params +SELECT strings.LTRIM(a, "hel !") FROM test; +/* result: +{ + "LTRIM(a, \"hel !\")": "o " +} +{ + "LTRIM(a, \"hel !\")": "o!" +} +{ + "LTRIM(a, \"hel !\")": "o! " +} +*/ + + +-- test: LTRIM BOOL +SELECT strings.LTRIM(true); +/* result: +{ + "LTRIM(true)": NULL +} +*/ + +-- test: LTRIM INT +SELECT strings.LTRIM(42); +/* result: +{ + "LTRIM(42)": NULL +} +*/ + +-- test: LTRIM DOUBLE +SELECT strings.LTRIM(42.42); +/* result: +{ + "LTRIM(42.42)": NULL +} +*/ + +-- test: LTRIM ARRAY +SELECT strings.LTRIM([1, 2]); +/* result: +{ + "LTRIM([1, 2])": NULL +} +*/ +-- test: LTRIM DOCUMENT +SELECT strings.LTRIM({a: 1}); +/* result: +{ + "LTRIM({a: 1})": NULL +} +*/ + +-- test: LTRIM STRING wrong param +SELECT strings.LTRIM(" hello ", 42); +/* result: +{ + "LTRIM(\" hello \", 42)": NULL +} +*/ diff --git a/sqltests/SELECT/STRINGS/rtrim.sql b/sqltests/SELECT/STRINGS/rtrim.sql new file mode 100644 index 00000000..e61295e1 --- /dev/null +++ b/sqltests/SELECT/STRINGS/rtrim.sql @@ -0,0 +1,112 @@ +-- setup: +CREATE TABLE test( + a TEXT +); + +INSERT INTO test (a) VALUES (" hello "), ("!hello!"), (" !hello! "); + +-- test: RTRIM TEXT default +SELECT strings.RTRIM(a) FROM test; +/* result: +{ + "RTRIM(a)": " hello" +} +{ + "RTRIM(a)": "!hello!" +} +{ + "RTRIM(a)": " !hello!" +} +*/ + + +-- test: RTRIM TEXT with param +SELECT strings.RTRIM(a, "!") FROM test; +/* result: +{ + "RTRIM(a, \"!\")": " hello " +} +{ + "RTRIM(a, \"!\")": "!hello" +} +{ + "RTRIM(a, \"!\")": " !hello! " +} +*/ + +-- test: RTRIM TEXT with multiple char params +SELECT strings.RTRIM(a, " !") FROM test; +/* result: +{ + "RTRIM(a, \" !\")": " hello" +} +{ + "RTRIM(a, \" !\")": "!hello" +} +{ + "RTRIM(a, \" !\")": " !hello" +} +*/ + + +-- test: RTRIM TEXT with multiple char params +SELECT strings.RTRIM(a, "hel !") FROM test; +/* result: +{ + "RTRIM(a, \"hel !\")": " hello" +} +{ + "RTRIM(a, \"hel !\")": "!hello" +} +{ + "RTRIM(a, \"hel !\")": " !hello" +} +*/ + + +-- test: RTRIM BOOL +SELECT strings.RTRIM(true); +/* result: +{ + "RTRIM(true)": NULL +} +*/ + +-- test: RTRIM INT +SELECT strings.RTRIM(42); +/* result: +{ + "RTRIM(42)": NULL +} +*/ + +-- test: RTRIM DOUBLE +SELECT strings.RTRIM(42.42); +/* result: +{ + "RTRIM(42.42)": NULL +} +*/ + +-- test: RTRIM ARRAY +SELECT strings.RTRIM([1, 2]); +/* result: +{ + "RTRIM([1, 2])": NULL +} +*/ +-- test: RTRIM DOCUMENT +SELECT strings.RTRIM({a: 1}); +/* result: +{ + "RTRIM({a: 1})": NULL +} +*/ + +-- test: RTRIM STRING wrong param +SELECT strings.RTRIM(" hello ", 42); +/* result: +{ + "RTRIM(\" hello \", 42)": NULL +} +*/ diff --git a/sqltests/SELECT/STRINGS/trim.sql b/sqltests/SELECT/STRINGS/trim.sql new file mode 100644 index 00000000..f5b1c7a4 --- /dev/null +++ b/sqltests/SELECT/STRINGS/trim.sql @@ -0,0 +1,112 @@ +-- setup: +CREATE TABLE test( + a TEXT +); + +INSERT INTO test (a) VALUES (" hello "), ("!hello!"), (" !hello! "); + +-- test: TRIM TEXT default +SELECT strings.TRIM(a) FROM test; +/* result: +{ + "TRIM(a)": "hello" +} +{ + "TRIM(a)": "!hello!" +} +{ + "TRIM(a)": "!hello!" +} +*/ + + +-- test: TRIM TEXT with param +SELECT strings.TRIM(a, "!") FROM test; +/* result: +{ + "TRIM(a, \"!\")": " hello " +} +{ + "TRIM(a, \"!\")": "hello" +} +{ + "TRIM(a, \"!\")": " !hello! " +} +*/ + +-- test: TRIM TEXT with multiple char params +SELECT strings.TRIM(a, " !") FROM test; +/* result: +{ + "TRIM(a, \" !\")": "hello" +} +{ + "TRIM(a, \" !\")": "hello" +} +{ + "TRIM(a, \" !\")": "hello" +} +*/ + + +-- test: TRIM TEXT with multiple char params +SELECT strings.TRIM(a, "hel !") FROM test; +/* result: +{ + "TRIM(a, \"hel !\")": "o" +} +{ + "TRIM(a, \"hel !\")": "o" +} +{ + "TRIM(a, \"hel !\")": "o" +} +*/ + + +-- test: TRIM BOOL +SELECT strings.TRIM(true); +/* result: +{ + "TRIM(true)": NULL +} +*/ + +-- test: TRIM INT +SELECT strings.TRIM(42); +/* result: +{ + "TRIM(42)": NULL +} +*/ + +-- test: TRIM DOUBLE +SELECT strings.TRIM(42.42); +/* result: +{ + "TRIM(42.42)": NULL +} +*/ + +-- test: TRIM ARRAY +SELECT strings.TRIM([1, 2]); +/* result: +{ + "TRIM([1, 2])": NULL +} +*/ +-- test: TRIM DOCUMENT +SELECT strings.TRIM({a: 1}); +/* result: +{ + "TRIM({a: 1})": NULL +} +*/ + +-- test: TRIM STRING wrong param +SELECT strings.TRIM(" hello ", 42); +/* result: +{ + "TRIM(\" hello \", 42)": NULL +} +*/