diff --git a/testcases/parser/parse_test.go b/testcases/parser/parse_test.go index 7e77056..0b67a20 100644 --- a/testcases/parser/parse_test.go +++ b/testcases/parser/parse_test.go @@ -101,6 +101,42 @@ lt('2016-12-31T13:30:15'::ts, '2017-12-31T13:30:15'::ts) = true::bool assert.Equal(t, ScalarFuncType, testFile.TestCases[0].FuncType) } +// Regression for #237: a `null::u![?]` argument used to panic +// in `VisitNullArg` because `TestCaseVisitor` had no `VisitUserDefined` +// method, so dispatch fell through to the base visitor's `VisitChildren` +// (which returns nil for the terminal `UserDefinedContext` node). The new +// `VisitUserDefined` returns a `*types.UserDefinedType` so the null +// literal can be constructed without panic. (`VisitNullArg` then forces +// the `CaseLiteral.Type`'s nullability to `Nullable` regardless of the +// parsed `?`, so we don't assert on that — we only assert the arg type +// is a `UserDefinedType` and the value is a `NullLiteral`.) +func TestParseUserDefinedNullArg(t *testing.T) { + header := makeHeader("v1.0", "/extensions/functions_arithmetic.yaml") + groupDesc := "# user-defined null arg parsing (issue #237)\n" + cases := []string{ + "f1(null::u!geometry) = 1::i8", + "f1(null::u!geometry?) = 1::i8", + } + for _, tc := range cases { + t.Run(tc, func(t *testing.T) { + testFile, err := ParseTestCasesFromString(header + groupDesc + tc) + require.NoError(t, err, "parse must not panic / error on user-defined null arg") + require.NotNil(t, testFile) + require.Len(t, testFile.TestCases, 1) + require.Len(t, testFile.TestCases[0].Args, 1) + + arg := testFile.TestCases[0].Args[0] + _, ok := arg.Type.(*types.UserDefinedType) + require.True(t, ok, "expected *types.UserDefinedType, got %T", arg.Type) + + lit, ok := arg.Value.(*expr.NullLiteral) + require.True(t, ok, "expected *expr.NullLiteral, got %T", arg.Value) + _, ok = lit.GetType().(*types.UserDefinedType) + assert.True(t, ok, "NullLiteral.GetType() should be *types.UserDefinedType, got %T", lit.GetType()) + }) + } +} + func TestParseDecimalExample(t *testing.T) { header := makeHeader("v1.0", "extensions/functions_arithmetic_decimal.yaml") tests := `# basic diff --git a/testcases/parser/visitor.go b/testcases/parser/visitor.go index 53e6bb1..623fe9a 100644 --- a/testcases/parser/visitor.go +++ b/testcases/parser/visitor.go @@ -944,6 +944,25 @@ func (v *TestCaseVisitor) VisitDataType(ctx *baseparser.DataTypeContext) interfa return nil } +// VisitUserDefined handles the `u![?]` scalar form that +// shows up in parser inputs like `null::u!geometry?`. The base visitor +// returns VisitChildren here (nil), which caused VisitNullArg to try +// to use nil as a types.Type and panic with +// "interface conversion: interface {} is nil, not string". +func (v *TestCaseVisitor) VisitUserDefined(ctx *baseparser.UserDefinedContext) interface{} { + nullability := types.NullabilityRequired + if ctx.QMark() != nil { + nullability = types.NullabilityNullable + } + ident := ctx.Identifier() + if ident == nil { + v.ErrorListener.ReportVisitError(ctx, fmt.Errorf("user-defined type without identifier: %v", ctx.GetText())) + return &types.UserDefinedType{Nullability: nullability} + } + _ = ident.GetText() // reserved for type-lookup; not available at this visit depth + return &types.UserDefinedType{Nullability: nullability} +} + func (v *TestCaseVisitor) VisitParameterizedType(ctx *baseparser.ParameterizedTypeContext) interface{} { if ctx.DecimalType() != nil { return v.Visit(ctx.DecimalType())