Skip to content

Commit

Permalink
expression, types: fix some corner case when parsing string to dateti…
Browse files Browse the repository at this point in the history
…me (pingcap#7701)
  • Loading branch information
spongedu authored and XuHuaiyu committed Oct 8, 2018
1 parent f13ae27 commit 5baedaa
Show file tree
Hide file tree
Showing 17 changed files with 141 additions and 83 deletions.
8 changes: 4 additions & 4 deletions expression/builtin_cast.go
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ func (b *builtinCastRealAsDurationSig) evalDuration(row chunk.Row) (res types.Du
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}
res, err = types.ParseDuration(strconv.FormatFloat(val, 'f', -1, 64), b.tp.Decimal)
res, err = types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, strconv.FormatFloat(val, 'f', -1, 64), b.tp.Decimal)
return res, false, errors.Trace(err)
}

Expand Down Expand Up @@ -994,7 +994,7 @@ func (b *builtinCastDecimalAsDurationSig) evalDuration(row chunk.Row) (res types
if isNull || err != nil {
return res, false, errors.Trace(err)
}
res, err = types.ParseDuration(string(val.ToString()), b.tp.Decimal)
res, err = types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, string(val.ToString()), b.tp.Decimal)
if types.ErrTruncatedWrongVal.Equal(err) {
err = b.ctx.GetSessionVars().StmtCtx.HandleTruncate(err)
// ZeroDuration of error ErrTruncatedWrongVal needs to be considered NULL.
Expand Down Expand Up @@ -1199,7 +1199,7 @@ func (b *builtinCastStringAsDurationSig) evalDuration(row chunk.Row) (res types.
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}
res, err = types.ParseDuration(val, b.tp.Decimal)
res, err = types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, val, b.tp.Decimal)
if types.ErrTruncatedWrongVal.Equal(err) {
sc := b.ctx.GetSessionVars().StmtCtx
err = sc.HandleTruncate(err)
Expand Down Expand Up @@ -1617,7 +1617,7 @@ func (b *builtinCastJSONAsDurationSig) evalDuration(row chunk.Row) (res types.Du
if err != nil {
return res, false, errors.Trace(err)
}
res, err = types.ParseDuration(s, b.tp.Decimal)
res, err = types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, s, b.tp.Decimal)
if types.ErrTruncatedWrongVal.Equal(err) {
sc := b.ctx.GetSessionVars().StmtCtx
err = sc.HandleTruncate(err)
Expand Down
49 changes: 25 additions & 24 deletions expression/builtin_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -2023,7 +2023,7 @@ func (b *builtinCurrentTime0ArgSig) Clone() builtinFunc {
func (b *builtinCurrentTime0ArgSig) evalDuration(row chunk.Row) (types.Duration, bool, error) {
tz := b.ctx.GetSessionVars().Location()
dur := time.Now().In(tz).Format(types.TimeFormat)
res, err := types.ParseDuration(dur, types.MinFsp)
res, err := types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, dur, types.MinFsp)
if err != nil {
return types.Duration{}, true, errors.Trace(err)
}
Expand All @@ -2047,7 +2047,7 @@ func (b *builtinCurrentTime1ArgSig) evalDuration(row chunk.Row) (types.Duration,
}
tz := b.ctx.GetSessionVars().Location()
dur := time.Now().In(tz).Format(types.TimeFSPFormat)
res, err := types.ParseDuration(dur, int(fsp))
res, err := types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, dur, int(fsp))
if err != nil {
return types.Duration{}, true, errors.Trace(err)
}
Expand Down Expand Up @@ -2099,9 +2099,9 @@ func (b *builtinTimeSig) evalDuration(row chunk.Row) (res types.Duration, isNull
return res, isNull, errors.Trace(err)
}

res, err = types.ParseDuration(expr, fsp)
sc := b.ctx.GetSessionVars().StmtCtx
res, err = types.ParseDuration(sc, expr, fsp)
if types.ErrTruncatedWrongVal.Equal(err) {
sc := b.ctx.GetSessionVars().StmtCtx
err = sc.HandleTruncate(err)
}
return res, isNull, errors.Trace(err)
Expand All @@ -2127,7 +2127,7 @@ func (c *timeLiteralFunctionClass) getFunction(ctx sessionctx.Context, args []Ex
if !isDuration(str) {
return nil, types.ErrIncorrectDatetimeValue.GenWithStackByArgs(str)
}
duration, err := types.ParseDuration(str, types.GetFsp(str))
duration, err := types.ParseDuration(ctx.GetSessionVars().StmtCtx, str, types.GetFsp(str))
if err != nil {
return nil, errors.Trace(err)
}
Expand Down Expand Up @@ -3584,11 +3584,11 @@ func (b *builtinTimestamp2ArgsSig) evalTime(row chunk.Row) (types.Time, bool, er
if !isDuration(arg1) {
return types.Time{}, true, nil
}
duration, err := types.ParseDuration(arg1, types.GetFsp(arg1))
duration, err := types.ParseDuration(sc, arg1, types.GetFsp(arg1))
if err != nil {
return types.Time{}, true, errors.Trace(handleInvalidTimeError(b.ctx, err))
}
tmp, err := tm.Add(b.ctx.GetSessionVars().StmtCtx, duration)
tmp, err := tm.Add(sc, duration)
if err != nil {
return types.Time{}, true, errors.Trace(err)
}
Expand Down Expand Up @@ -3735,7 +3735,7 @@ func strDatetimeAddDuration(sc *stmtctx.StatementContext, d string, arg1 types.D

// strDurationAddDuration adds duration to duration string, returns a string value.
func strDurationAddDuration(sc *stmtctx.StatementContext, d string, arg1 types.Duration) (string, error) {
arg0, err := types.ParseDuration(d, types.MaxFsp)
arg0, err := types.ParseDuration(sc, d, types.MaxFsp)
if err != nil {
return "", errors.Trace(err)
}
Expand Down Expand Up @@ -3775,7 +3775,7 @@ func strDatetimeSubDuration(sc *stmtctx.StatementContext, d string, arg1 types.D

// strDurationSubDuration subtracts duration from duration string, returns a string value.
func strDurationSubDuration(sc *stmtctx.StatementContext, d string, arg1 types.Duration) (string, error) {
arg0, err := types.ParseDuration(d, types.MaxFsp)
arg0, err := types.ParseDuration(sc, d, types.MaxFsp)
if err != nil {
return "", errors.Trace(err)
}
Expand Down Expand Up @@ -3908,11 +3908,12 @@ func (b *builtinAddDatetimeAndStringSig) evalTime(row chunk.Row) (types.Time, bo
if !isDuration(s) {
return types.ZeroDatetime, true, nil
}
arg1, err := types.ParseDuration(s, types.GetFsp(s))
sc := b.ctx.GetSessionVars().StmtCtx
arg1, err := types.ParseDuration(sc, s, types.GetFsp(s))
if err != nil {
return types.ZeroDatetime, true, errors.Trace(err)
}
result, err := arg0.Add(b.ctx.GetSessionVars().StmtCtx, arg1)
result, err := arg0.Add(sc, arg1)
return result, err != nil, errors.Trace(err)
}

Expand Down Expand Up @@ -3984,7 +3985,7 @@ func (b *builtinAddDurationAndStringSig) evalDuration(row chunk.Row) (types.Dura
if !isDuration(s) {
return types.ZeroDuration, true, nil
}
arg1, err := types.ParseDuration(s, types.GetFsp(s))
arg1, err := types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, s, types.GetFsp(s))
if err != nil {
return types.ZeroDuration, true, errors.Trace(err)
}
Expand Down Expand Up @@ -4073,11 +4074,11 @@ func (b *builtinAddStringAndStringSig) evalString(row chunk.Row) (result string,
if isNull || err != nil {
return "", isNull, errors.Trace(err)
}
arg1, err = types.ParseDuration(arg1Str, getFsp4TimeAddSub(arg1Str))
sc := b.ctx.GetSessionVars().StmtCtx
arg1, err = types.ParseDuration(sc, arg1Str, getFsp4TimeAddSub(arg1Str))
if err != nil {
return "", true, errors.Trace(err)
}
sc := b.ctx.GetSessionVars().StmtCtx
if isDuration(arg0) {
result, err = strDurationAddDuration(sc, arg0, arg1)
if err != nil {
Expand Down Expand Up @@ -4138,7 +4139,7 @@ func (b *builtinAddDateAndStringSig) evalString(row chunk.Row) (string, bool, er
if !isDuration(s) {
return "", true, nil
}
arg1, err := types.ParseDuration(s, getFsp4TimeAddSub(s))
arg1, err := types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, s, getFsp4TimeAddSub(s))
if err != nil {
return "", true, errors.Trace(err)
}
Expand Down Expand Up @@ -4406,7 +4407,7 @@ func (b *builtinMakeTimeSig) evalDuration(row chunk.Row) (types.Duration, bool,
second = 59
}
fsp := b.tp.Decimal
dur, err = types.ParseDuration(fmt.Sprintf("%02d:%02d:%v", hour, minute, second), fsp)
dur, err = types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, fmt.Sprintf("%02d:%02d:%v", hour, minute, second), fsp)
if err != nil {
return dur, true, errors.Trace(err)
}
Expand Down Expand Up @@ -4649,7 +4650,7 @@ func (b *builtinSecToTimeSig) evalDuration(row chunk.Row) (types.Duration, bool,
secondDemical = float64(second) + demical

var dur types.Duration
dur, err = types.ParseDuration(fmt.Sprintf("%s%02d:%02d:%v", negative, hour, minute, secondDemical), b.tp.Decimal)
dur, err = types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, fmt.Sprintf("%s%02d:%02d:%v", negative, hour, minute, secondDemical), b.tp.Decimal)
if err != nil {
return types.Duration{}, err != nil, errors.Trace(err)
}
Expand Down Expand Up @@ -4767,11 +4768,11 @@ func (b *builtinSubDatetimeAndStringSig) evalTime(row chunk.Row) (types.Time, bo
if !isDuration(s) {
return types.ZeroDatetime, true, nil
}
arg1, err := types.ParseDuration(s, types.GetFsp(s))
sc := b.ctx.GetSessionVars().StmtCtx
arg1, err := types.ParseDuration(sc, s, types.GetFsp(s))
if err != nil {
return types.ZeroDatetime, true, errors.Trace(err)
}
sc := b.ctx.GetSessionVars().StmtCtx
arg1time, err := arg1.ConvertToTime(sc, mysql.TypeDatetime)
if err != nil {
return types.ZeroDatetime, true, errors.Trace(err)
Expand Down Expand Up @@ -4859,7 +4860,7 @@ func (b *builtinSubStringAndStringSig) evalString(row chunk.Row) (result string,
if isNull || err != nil {
return "", isNull, errors.Trace(err)
}
arg1, err = types.ParseDuration(s, getFsp4TimeAddSub(s))
arg1, err = types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, s, getFsp4TimeAddSub(s))
if err != nil {
return "", true, errors.Trace(err)
}
Expand Down Expand Up @@ -4943,7 +4944,7 @@ func (b *builtinSubDurationAndStringSig) evalDuration(row chunk.Row) (types.Dura
if !isDuration(s) {
return types.ZeroDuration, true, nil
}
arg1, err := types.ParseDuration(s, types.GetFsp(s))
arg1, err := types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, s, types.GetFsp(s))
if err != nil {
return types.ZeroDuration, true, errors.Trace(err)
}
Expand Down Expand Up @@ -5016,7 +5017,7 @@ func (b *builtinSubDateAndStringSig) evalString(row chunk.Row) (string, bool, er
if !isDuration(s) {
return "", true, nil
}
arg1, err := types.ParseDuration(s, getFsp4TimeAddSub(s))
arg1, err := types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, s, getFsp4TimeAddSub(s))
if err != nil {
return "", true, errors.Trace(err)
}
Expand Down Expand Up @@ -5332,7 +5333,7 @@ func (b *builtinUTCTimeWithoutArgSig) Clone() builtinFunc {
// evalDuration evals a builtinUTCTimeWithoutArgSig.
// See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_utc-time
func (b *builtinUTCTimeWithoutArgSig) evalDuration(row chunk.Row) (types.Duration, bool, error) {
v, err := types.ParseDuration(time.Now().UTC().Format(types.TimeFormat), 0)
v, err := types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, time.Now().UTC().Format(types.TimeFormat), 0)
return v, false, err
}

Expand All @@ -5359,7 +5360,7 @@ func (b *builtinUTCTimeWithArgSig) evalDuration(row chunk.Row) (types.Duration,
if fsp < int64(types.MinFsp) {
return types.Duration{}, true, errors.Errorf("Invalid negative %d specified, must in [0, 6].", fsp)
}
v, err := types.ParseDuration(time.Now().UTC().Format(types.TimeFSPFormat), int(fsp))
v, err := types.ParseDuration(b.ctx.GetSessionVars().StmtCtx, time.Now().UTC().Format(types.TimeFSPFormat), int(fsp))
return v, false, err
}

Expand Down
4 changes: 2 additions & 2 deletions expression/builtin_time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ func (s *testEvaluatorSuite) TestAddTimeSig(c *C) {
{"-110:00:00", "1 02:00:00", "-84:00:00"},
}
for _, t := range tbl {
dur, err := types.ParseDuration(t.Input, types.GetFsp(t.Input))
dur, err := types.ParseDuration(s.ctx.GetSessionVars().StmtCtx, t.Input, types.GetFsp(t.Input))
c.Assert(err, IsNil)
tmpInput := types.NewDurationDatum(dur)
tmpInputDuration := types.NewStringDatum(t.InputDuration)
Expand Down Expand Up @@ -962,7 +962,7 @@ func (s *testEvaluatorSuite) TestSubTimeSig(c *C) {
{"235959", "00:00:01", "23:59:58"},
}
for _, t := range tbl {
dur, err := types.ParseDuration(t.Input, types.GetFsp(t.Input))
dur, err := types.ParseDuration(s.ctx.GetSessionVars().StmtCtx, t.Input, types.GetFsp(t.Input))
c.Assert(err, IsNil)
tmpInput := types.NewDurationDatum(dur)
tmpInputDuration := types.NewStringDatum(t.InputDuration)
Expand Down
2 changes: 1 addition & 1 deletion expression/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func (c *Constant) EvalDuration(ctx sessionctx.Context, _ chunk.Row) (val types.
if err != nil {
return types.Duration{}, true, errors.Trace(err)
}
dur, err := types.ParseDuration(val, types.MaxFsp)
dur, err := types.ParseDuration(ctx.GetSessionVars().StmtCtx, val, types.MaxFsp)
if err != nil {
return types.Duration{}, true, errors.Trace(err)
}
Expand Down
2 changes: 1 addition & 1 deletion expression/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func init() {

// handleInvalidTimeError reports error or warning depend on the context.
func handleInvalidTimeError(ctx sessionctx.Context, err error) error {
if err == nil || !(terror.ErrorEqual(err, types.ErrInvalidTimeFormat) || types.ErrIncorrectDatetimeValue.Equal(err)) {
if err == nil || !(terror.ErrorEqual(err, types.ErrInvalidTimeFormat) || types.ErrIncorrectDatetimeValue.Equal(err) || types.ErrTruncatedWrongValue.Equal(err)) {
return err
}
sc := ctx.GetSessionVars().StmtCtx
Expand Down
38 changes: 38 additions & 0 deletions expression/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,7 @@ func (s *testIntegrationSuite) TestBuiltin(c *C) {
result = tk.MustQuery(`select cast(20170118.999 as datetime);`)
result.Check(testkit.Rows("2017-01-18 00:00:00"))

// Test corner cases of cast string as datetime
result = tk.MustQuery(`select cast("170102034" as datetime);`)
result.Check(testkit.Rows("2017-01-02 03:04:00"))
result = tk.MustQuery(`select cast("1701020304" as datetime);`)
Expand All @@ -1944,6 +1945,43 @@ func (s *testIntegrationSuite) TestBuiltin(c *C) {
result = tk.MustQuery(`select cast("1701020304.111" as datetime);`)
result.Check(testkit.Rows("2017-01-02 03:04:11"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '1701020304.111'"))
result = tk.MustQuery(`select cast("17011" as datetime);`)
result.Check(testkit.Rows("2017-01-01 00:00:00"))
result = tk.MustQuery(`select cast("150101." as datetime);`)
result.Check(testkit.Rows("2015-01-01 00:00:00"))
result = tk.MustQuery(`select cast("150101.a" as datetime);`)
result.Check(testkit.Rows("2015-01-01 00:00:00"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '150101.a'"))
result = tk.MustQuery(`select cast("150101.1a" as datetime);`)
result.Check(testkit.Rows("2015-01-01 01:00:00"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '150101.1a'"))
result = tk.MustQuery(`select cast("150101.1a1" as datetime);`)
result.Check(testkit.Rows("2015-01-01 01:00:00"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '150101.1a1'"))
result = tk.MustQuery(`select cast("1101010101.111" as datetime);`)
result.Check(testkit.Rows("2011-01-01 01:01:11"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '1101010101.111'"))
result = tk.MustQuery(`select cast("1101010101.11aaaaa" as datetime);`)
result.Check(testkit.Rows("2011-01-01 01:01:11"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '1101010101.11aaaaa'"))
result = tk.MustQuery(`select cast("1101010101.a1aaaaa" as datetime);`)
result.Check(testkit.Rows("2011-01-01 01:01:00"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '1101010101.a1aaaaa'"))
result = tk.MustQuery(`select cast("1101010101.11" as datetime);`)
result.Check(testkit.Rows("2011-01-01 01:01:11"))
tk.MustQuery("select @@warning_count;").Check(testkit.Rows("0"))
result = tk.MustQuery(`select cast("1101010101.111" as datetime);`)
result.Check(testkit.Rows("2011-01-01 01:01:11"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '1101010101.111'"))
result = tk.MustQuery(`select cast("970101.111" as datetime);`)
result.Check(testkit.Rows("1997-01-01 11:01:00"))
tk.MustQuery("select @@warning_count;").Check(testkit.Rows("0"))
result = tk.MustQuery(`select cast("970101.11111" as datetime);`)
result.Check(testkit.Rows("1997-01-01 11:11:01"))
tk.MustQuery("select @@warning_count;").Check(testkit.Rows("0"))
result = tk.MustQuery(`select cast("970101.111a1" as datetime);`)
result.Check(testkit.Rows("1997-01-01 11:01:00"))
tk.MustQuery("show warnings;").Check(testkit.Rows("Warning 1292 Truncated incorrect datetime value: '970101.111a1'"))

// for ISNULL
tk.MustExec("drop table if exists t")
Expand Down
4 changes: 2 additions & 2 deletions server/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (s *testUtilSuite) TestDumpBinaryTime(c *C) {
c.Assert(err, IsNil)
c.Assert(d, DeepEquals, []byte{4, 0, 0, 0, 0})

myDuration, err := types.ParseDuration("0000-00-00 00:00:00.0000000", 6)
myDuration, err := types.ParseDuration(nil, "0000-00-00 00:00:00.0000000", 6)
c.Assert(err, IsNil)
d = dumpBinaryTime(myDuration.Duration)
c.Assert(d, DeepEquals, []byte{0})
Expand Down Expand Up @@ -139,7 +139,7 @@ func (s *testUtilSuite) TestDumpTextValue(c *C) {
c.Assert(err, IsNil)
c.Assert(mustDecodeStr(c, bs), Equals, "2017-01-06 00:00:00")

duration, err := types.ParseDuration("11:30:45", 0)
duration, err := types.ParseDuration(nil, "11:30:45", 0)
c.Assert(err, IsNil)
d.SetMysqlDuration(duration)
columns[0].Type = mysql.TypeDuration
Expand Down
2 changes: 1 addition & 1 deletion sessionctx/variable/varsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ func parseTimeZone(s string) (*time.Location, error) {

// The value can be given as a string indicating an offset from UTC, such as '+10:00' or '-6:00'.
if strings.HasPrefix(s, "+") || strings.HasPrefix(s, "-") {
d, err := types.ParseDuration(s[1:], 0)
d, err := types.ParseDuration(nil, s[1:], 0)
if err == nil {
ofst := int(d.Duration / time.Second)
if s[0] == '-' {
Expand Down
2 changes: 1 addition & 1 deletion statistics/scalar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func getDecimal(value float64) *types.MyDecimal {
}

func getDuration(value string) types.Duration {
dur, _ := types.ParseDuration(value, 0)
dur, _ := types.ParseDuration(nil, value, 0)
return dur
}

Expand Down
2 changes: 1 addition & 1 deletion tablecodec/tablecodec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func (s *testTableCodecSuite) TestTimeCodec(c *C) {
"2016-06-23 11:30:45")
c.Assert(err, IsNil)
row[2] = types.NewDatum(ts)
du, err := types.ParseDuration("12:59:59.999999", 6)
du, err := types.ParseDuration(nil, "12:59:59.999999", 6)
c.Assert(err, IsNil)
row[3] = types.NewDatum(du)

Expand Down
2 changes: 1 addition & 1 deletion types/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func StrToDuration(sc *stmtctx.StatementContext, str string, fsp int) (d Duratio
}
}

d, err = ParseDuration(str, fsp)
d, err = ParseDuration(sc, str, fsp)
if ErrTruncatedWrongVal.Equal(err) {
err = sc.HandleTruncate(err)
}
Expand Down
2 changes: 1 addition & 1 deletion types/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ func (s *testTypeConvertSuite) TestConvertToString(c *C) {
c.Assert(err, IsNil)
testToString(c, t, "2011-11-10 11:11:11.999999")

td, err := ParseDuration("11:11:11.999999", 6)
td, err := ParseDuration(nil, "11:11:11.999999", 6)
c.Assert(err, IsNil)
testToString(c, td, "11:11:11.999999")

Expand Down
Loading

0 comments on commit 5baedaa

Please sign in to comment.