From 5c6361918a2d77d03f09dabf0254d9a29c9078de Mon Sep 17 00:00:00 2001 From: Bruce Baranowski <92940816+bruce-b-molecula@users.noreply.github.com> Date: Tue, 14 Feb 2023 15:58:36 -0500 Subject: [PATCH] FB-1862: Implement Str() scalar string function (#2215) * Implement STR() --- sql3/planner/expression.go | 2 + sql3/planner/expressionanalyzercall.go | 2 + sql3/planner/inbuiltfunctionsstring.go | 83 +++++++++++++++ sql3/test/defs/defs_string_functions.go | 130 ++++++++++++++++++++++++ 4 files changed, 217 insertions(+) diff --git a/sql3/planner/expression.go b/sql3/planner/expression.go index 8edb9a6ac..de33dd367 100644 --- a/sql3/planner/expression.go +++ b/sql3/planner/expression.go @@ -1558,6 +1558,8 @@ func (n *callPlanExpression) Evaluate(currentRow []interface{}) (interface{}, er return n.EvaluateFormat(currentRow) case "CHARINDEX": return n.EvaluateCharIndex(currentRow) + case "STR": + return n.EvaluateStr(currentRow) default: return nil, sql3.NewErrInternalf("unhandled function name '%s'", n.name) } diff --git a/sql3/planner/expressionanalyzercall.go b/sql3/planner/expressionanalyzercall.go index 90d13d522..f38af57b9 100644 --- a/sql3/planner/expressionanalyzercall.go +++ b/sql3/planner/expressionanalyzercall.go @@ -278,6 +278,8 @@ func (p *ExecutionPlanner) analyzeCallExpression(ctx context.Context, call *pars return p.analyseFunctionFormat(call, scope) case "CHARINDEX": return p.analyseFunctionCharIndex(call, scope) + case "STR": + return p.analyseFunctionStr(call, scope) default: return nil, sql3.NewErrCallUnknownFunction(call.Name.NamePos.Line, call.Name.NamePos.Column, call.Name.Name) } diff --git a/sql3/planner/inbuiltfunctionsstring.go b/sql3/planner/inbuiltfunctionsstring.go index 07cc42dfe..865ab42af 100644 --- a/sql3/planner/inbuiltfunctionsstring.go +++ b/sql3/planner/inbuiltfunctionsstring.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/featurebasedb/featurebase/v3/pql" "github.com/featurebasedb/featurebase/v3/sql3" "github.com/featurebasedb/featurebase/v3/sql3/parser" ) @@ -783,3 +784,85 @@ func (n *callPlanExpression) EvaluateCharIndex(currentRow []interface{}) (interf } return int64(res), nil } + +func (p *ExecutionPlanner) analyseFunctionStr(call *parser.Call, scope parser.Statement) (parser.Expr, error) { + if len(call.Args) < 1 || len(call.Args) > 3 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 1, len(call.Args)) + } + targetType := parser.NewDataTypeDecimal(4) + if !typesAreAssignmentCompatible(targetType, call.Args[0].DataType()) { + return nil, sql3.NewErrTypeAssignmentIncompatible(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Args[0].DataType().TypeDescription(), targetType.TypeDescription()) + } + + // the second and third parameters are optional + for i := 1; i < len(call.Args); i++ { + if typeIsVoid(call.Args[i].DataType()) { + return nil, sql3.NewErrLiteralNullNotAllowed(call.Args[i].Pos().Line, call.Args[i].Pos().Column) + } + if !typeIsInteger(call.Args[i].DataType()) { + return nil, sql3.NewErrIntExpressionExpected(call.Args[i].Pos().Line, call.Args[i].Pos().Column) + } + } + + call.ResultDataType = parser.NewDataTypeString() + + return call, nil +} + +func (n *callPlanExpression) EvaluateStr(currentRow []interface{}) (interface{}, error) { + argOneEval, err := n.args[0].Evaluate(currentRow) + if err != nil { + return "", err + } + if argOneEval == nil { + return nil, nil + } + + coercedValue, err := coerceValue(n.args[0].Type(), parser.NewDataTypeDecimal(4), argOneEval, parser.Pos{}) + if err != nil { + return "", err + } + + decimalArgOne, ok := coercedValue.(pql.Decimal) + if !ok { + return nil, sql3.NewErrUnexpectedTypeConversion(0, 0, coercedValue) + } + + intArgTwo := int64(10) + if len(n.args) > 1 { + argEval, err := n.args[1].Evaluate(currentRow) + if err != nil { + return nil, err + } + intArgTwo, ok = argEval.(int64) + if !ok { + return nil, sql3.NewErrUnexpectedTypeConversion(0, 0, argEval) + } + } + + intArgThree := int64(0) + if len(n.args) > 2 { + argEval, err := n.args[2].Evaluate(currentRow) + if err != nil { + return nil, err + } + intArgThree, ok = argEval.(int64) + if !ok { + return nil, sql3.NewErrUnexpectedTypeConversion(0, 0, argEval) + } + } + + floatValue := decimalArgOne.Float64() + strFormat := fmt.Sprintf("%%%d.%df", intArgTwo, intArgThree) + decimalString := fmt.Sprintf(strFormat, floatValue) + + // check if value can be displayed within allocated length + if int64(len(decimalString)) > intArgTwo { + decimalString = "" + for i := int64(0); i < intArgTwo; i++ { + decimalString += "*" + } + } + + return decimalString, nil +} diff --git a/sql3/test/defs/defs_string_functions.go b/sql3/test/defs/defs_string_functions.go index 8b066c36a..3987c3a76 100644 --- a/sql3/test/defs/defs_string_functions.go +++ b/sql3/test/defs/defs_string_functions.go @@ -982,5 +982,135 @@ var stringScalarFunctionsTests = TableTest{ ), Compare: CompareExactOrdered, }, + { + name: "StrIntValue", + SQLs: sqls( + "select str(12345)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(string(" 12345")), + ), + Compare: CompareExactOrdered, + }, + { + name: "StrIntValueAtEdge", + SQLs: sqls( + "select str(12345, 5)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(string("12345")), + ), + Compare: CompareExactOrdered, + }, + { + name: "StrIntValueWithPrecision", + SQLs: sqls( + "select str(12345, 5, 5)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(string("*****")), + ), + Compare: CompareExactOrdered, + }, + { + name: "StrDecimalValue", + SQLs: sqls( + "select str(12345.678)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(string(" 12346")), + ), + Compare: CompareExactOrdered, + }, + { + name: "StrDecimalValueAtEdge", + SQLs: sqls( + "select str(12345.19, 5)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(string("12345")), + ), + Compare: CompareExactOrdered, + }, + { + name: "StrDecimalValueWithPrecision", + SQLs: sqls( + "select str(12345.789, 8, 2)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(string("12345.79")), + ), + Compare: CompareExactOrdered, + }, + { + name: "StrNegativeDecimalValueWithPrecision", + SQLs: sqls( + "select str(-2345.789, 8, 2)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(string("-2345.79")), + ), + Compare: CompareExactOrdered, + }, + { + name: "TooFewArgumentsforStr", + SQLs: sqls( + "select str()", + ), + ExpErr: "'str': count of formal parameters (1) does not match count of actual parameters (0)", + }, + { + name: "TooManyArgumentsforStr", + SQLs: sqls( + "select str(1, 1, 1, 1)", + ), + ExpErr: "'str': count of formal parameters (1) does not match count of actual parameters (4)", + }, + { + name: "StrPrecisionLargerThanValue", + SQLs: sqls( + "select str(1234.99, 10, 200)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(string("**********")), + ), + Compare: CompareExactOrdered, + }, { + name: "StrNull", + SQLs: sqls( + "select str(null)", + ), + ExpHdrs: hdrs( + hdr("", fldTypeString), + ), + ExpRows: rows( + row(nil), + ), + Compare: CompareExactUnordered, + }, }, }