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 TIME_TRUNCATE_FRACTIONAL sql_mode #56442

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 pkg/ddl/reorg.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ func reorgTypeFlagsWithSQLMode(mode mysql.SQLMode) types.Flags {
WithTruncateAsWarning(!mode.HasStrictMode()).
WithIgnoreInvalidDateErr(mode.HasAllowInvalidDatesMode()).
WithIgnoreZeroInDate(!mode.HasStrictMode() || mode.HasAllowInvalidDatesMode()).
WithCastTimeToYearThroughConcat(true)
WithCastTimeToYearThroughConcat(true).
WithTimeTruncateFractional(mode.HasTimeTruncateFractional())
}

func reorgErrLevelsWithSQLMode(mode mysql.SQLMode) errctx.LevelMap {
Expand Down
3 changes: 2 additions & 1 deletion pkg/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2092,7 +2092,8 @@ func ResetContextOfStmt(ctx sessionctx.Context, s ast.StmtNode) (err error) {
// WithAllowNegativeToUnsigned with false value indicates values less than 0 should be clipped to 0 for unsigned integer types.
// This is the case for `insert`, `update`, `alter table`, `create table` and `load data infile` statements, when not in strict SQL mode.
// see https://dev.mysql.com/doc/refman/5.7/en/out-of-range-and-overflow.html
WithAllowNegativeToUnsigned(!sc.InInsertStmt && !sc.InLoadDataStmt && !sc.InUpdateStmt && !sc.InCreateOrAlterStmt),
WithAllowNegativeToUnsigned(!sc.InInsertStmt && !sc.InLoadDataStmt && !sc.InUpdateStmt && !sc.InCreateOrAlterStmt).
WithTimeTruncateFractional(vars.SQLMode.HasTimeTruncateFractional()),
)

vars.PlanCacheParams.Reset()
Expand Down
6 changes: 5 additions & 1 deletion pkg/expression/builtin_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -1733,7 +1733,11 @@ func evalFromUnixTime(ctx EvalContext, fsp int, unixTimeStamp *types.MyDecimal)

tc := typeCtx(ctx)
tmp := time.Unix(integralPart, fractionalPart).In(tc.Location())
t, err := convertTimeToMysqlTime(tmp, fsp, types.ModeHalfUp)
truncateMode := types.ModeHalfUp
if ctx.SQLMode().HasTimeTruncateFractional() {
truncateMode = types.ModeTruncate
}
t, err := convertTimeToMysqlTime(tmp, fsp, truncateMode)
if err != nil {
return res, true, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/expression/integration_test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ go_test(
"main_test.go",
],
flaky = True,
shard_count = 46,
shard_count = 49,
deps = [
"//pkg/config",
"//pkg/domain",
Expand Down
139 changes: 139 additions & 0 deletions pkg/expression/integration_test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,145 @@ func TestIssue9710(t *testing.T) {
}
}

func TestTimeFuncTruncation(t *testing.T) {
store := testkit.CreateMockStore(t)

tk := testkit.NewTestKit(t, store)
tk.MustExec("use test")
tk.MustExec(`set @@time_zone = 'UTC'`)
var res *testkit.Result
dateStr := `2023-12-31 23:59:59.9876789`
timeStr := "1704067199.9876789"
dateStrLow := `2023-12-31 23:59:59.12344321`
timeStrLow := "1704067199.12344321"
dateStrMid := `2023-12-31 23:59:59.4999996`
timeStrMid := "1704067199.4999996"
timeT9H := "1704067199.987678900"
timeT9M := "1704067199.499999600"
timeT9L := "1704067199.123443210"
for _, sqlMode := range []string{"TIME_TRUNCATE_FRACTIONAL", ""} {
tk.MustExec(`set @@sql_mode = DEFAULT`)
frac6H := "2023-12-31 23:59:59.987679"
frac6L := "2023-12-31 23:59:59.123443"
frac6M := "2023-12-31 23:59:59.500000"
frac0H := "2024-01-01 00:00:00"
frac0L := "2023-12-31 23:59:59"
dateRoundH := "2024-01-01"
dateRoundL := "2023-12-31"
// Y - Year, D - Day, H - High, L - Low
// timeT/unix_timestamp for '2024-01-01'
timeTY := "1704067200"
timeTH := timeTY
timeTL := "1704067199"
// timeT/unix_timestamp for '2023-12-31'
timeTDL := "1703980800"
timeTDH := timeTY
timeT6H := "1704067199.987679"
timeT6M := "1704067199.500000"
timeT6L := "1704067199.123443"
if sqlMode != "" {
res = tk.MustQuery(`select @@sql_mode`)
tk.MustExec(`set sql_mode = 'time_truncate_fractional,` + res.Rows()[0][0].(string) + `'`)
tk.MustQuery(`select @@sql_mode`).CheckContain("TIME_TRUNCATE_FRACTIONAL")
frac6H = "2023-12-31 23:59:59.987678"
frac0H = "2023-12-31 23:59:59"
frac6M = "2023-12-31 23:59:59.499999"
dateRoundH = "2023-12-31"
timeT6H = "1704067199.987678"
timeT6M = "1704067199.499999"
timeTH = timeTL
timeTDH = timeTDL
}

tk.MustExec(`drop table if exists t`)
// time_t not treated as time, but just normal int, so rounded!
tk.MustExec(`create table t (id int primary key, d date, dt datetime, dt6 datetime(6), ts timestamp, ts6 timestamp(6), time_t bigint, dc decimal(20,9))`)
tk.MustExec(`insert into t values (1, ?, ?, ?, ?, ?, ?, ?)`,
dateStr, dateStr, dateStr, dateStr, dateStr, timeStr, timeStr)
tk.MustExec(`insert into t values (2, ?, ?, ?, ?, ?, ?, ?)`,
dateStrLow, dateStrLow, dateStrLow, dateStrLow, dateStrLow, timeStrLow, timeStrLow)
tk.MustExec(`insert into t values (3, ?, ?, ?, ?, ?, ?, ?)`,
dateStrMid, dateStrMid, dateStrMid, dateStrMid, dateStrMid, timeStrMid, timeStrMid)

res = tk.MustQuery(`select * from t order by id /* ` + sqlMode + ` */`)
res.Check(testkit.Rows(""+
strings.Join([]string{"1", dateRoundH, frac0H, frac6H, frac0H, frac6H, timeTY, timeT9H}, " "),
strings.Join([]string{"2", dateRoundL, frac0L, frac6L, frac0L, frac6L, timeTL, timeT9L}, " "),
strings.Join([]string{"3", dateRoundL, frac0L, frac6M, frac0L, frac6M, timeTL, timeT9M}, " ")))
// Test functions that take datetime as arguments
tk.MustQuery(`select unix_timestamp(d), unix_timestamp(dt), unix_timestamp(dt6), unix_timestamp(ts), unix_timestamp(ts6) from t order by id /* ` + sqlMode + ` */`).Check(testkit.Rows(
// dt/ts is already truncated, so second will be the low one.
strings.Join([]string{timeTDH, timeTH, timeT6H, timeTH, timeT6H}, " "),
strings.Join([]string{timeTDL, timeTL, timeT6L, timeTL, timeT6L}, " "),
strings.Join([]string{timeTDL, timeTL, timeT6M, timeTL, timeT6M}, " ")))
// Test functions that take unix_timestamp (time_t) as argument
tk.MustQuery(`select from_unixtime(dc) from t order by id /* ` + sqlMode + ` */`).Check(testkit.Rows(frac6H, frac6L, frac6M))
tk.MustExec(`update t set d = from_unixtime(dc), dt = from_unixtime(dc), dt6 = from_unixtime(dc), ts = from_unixtime(dc), ts6 = from_unixtime(dc)`)
res2 := tk.MustQuery(`select * from t order by id /* ` + sqlMode + ` */`)
rows := res.Sort().Rows()
if sqlMode == "" {
// See below, TestDatetimeStringUnixTime!
rows[0][1] = dateRoundL
rows[2][2] = frac0H
rows[2][4] = frac0H
}
res2.Sort().Check(rows)
tk.MustQuery(`select from_unixtime(dc) from t order by id /* ` + sqlMode + ` */`).Check(testkit.Rows(frac6H, frac6L, frac6M))
}
}

func TestDatetimeStringUnixTime(t *testing.T) {
store := testkit.CreateMockStore(t)

tk := testkit.NewTestKit(t, store)
tk.MustExec("use test")
tk.MustExec("set time_zone = 'UTC'")
tk.MustExec("create table t (dS date, dU date, dtS datetime, dtU datetime)")
tk.MustExec(`insert into t values (` +
// convertToMysqlTime will round a datetime string to correct FSP (including FSP+1 digit),
// including overflow. It will set the type as DATE,
// and truncate the time portion, so date will be 2024-01-01.
`'2023-12-31 23:59:59.9876789',` +
// from_unixtime() will round the 6th FSP => 2023-12-31 23:59:59.987679,
// and returned as DATETIME(6) internally.
// It will then be CASTed to DATE, by just changing the type from TypeDateTime to TypeDate,
// and not doing any other rounding, since Date has no fsp, see RoundFrac().
// And lastly it will rewrite the date to only use the year,month and day.
// effectively truncate it.
`from_unixtime(1704067199.9876789),` +
// This will be parsed and rounded as a string with fsp = 0, (taking fsp+1 into account)
// -> rounded DOWN to datetime(0).
`'2023-12-31 23:59:59.4999996',` +
// from_unixtime() will round to FSP 6, so round UP here => .500000 as fraction part
// which is then rounded UP when Casting from DATETIME(6) to DATETIME(0)
`from_unixtime(1704067199.4999996))`)
tk.MustQuery(`select * from t`).Check(testkit.Rows("2024-01-01 2023-12-31 2023-12-31 23:59:59 2024-01-01 00:00:00"))
}

func TestDatetimeModifyTruncate(t *testing.T) {
store := testkit.CreateMockStore(t)

tk := testkit.NewTestKit(t, store)
tk.MustExec("use test")
tk.MustExec("set time_zone = 'UTC'")
tk.MustExec("create table t (d datetime(6))")
tk.MustExec(`insert into t values ('2023-12-31 23:59:59.9876789')`)
tk.MustExec(`insert into t values ('2023-12-31 23:59:59.4321234')`)
tk.MustExec(`alter table t modify column d datetime(3)`)
tk.MustQuery(`select * from t`).Sort().Check(testkit.Rows("2023-12-31 23:59:59.432", "2023-12-31 23:59:59.988"))
tk.MustExec(`alter table t modify column d datetime(0)`)
tk.MustQuery(`select * from t`).Sort().Check(testkit.Rows("2023-12-31 23:59:59", "2024-01-01 00:00:00"))
tk.MustExec(`drop table t`)
tk.MustExec(`set sql_mode=concat(@@sql_mode, ',time_truncate_fractional')`)
tk.MustExec("create table t (d datetime(6))")
tk.MustExec(`insert into t values ('2023-12-31 23:59:59.9876789')`)
tk.MustExec(`insert into t values ('2023-12-31 23:59:59.4321234')`)
tk.MustExec(`alter table t modify column d datetime(3)`)
tk.MustQuery(`select * from t`).Sort().Check(testkit.Rows("2023-12-31 23:59:59.432", "2023-12-31 23:59:59.987"))
tk.MustExec(`alter table t modify column d datetime(0)`)
tk.MustQuery(`select * from t`).Sort().Check(testkit.Rows("2023-12-31 23:59:59", "2023-12-31 23:59:59"))
}

func TestShardIndexOnTiFlash(t *testing.T) {
store := testkit.CreateMockStore(t)

Expand Down
7 changes: 7 additions & 0 deletions pkg/parser/mysql/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ var DefaultAuthPlugins = []string{
// See https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html
type SQLMode int64

// HasTimeTruncateFractional returns true if the 'TIME_TRUNCATE_FRACTIONAL' SQLMode is set
func (m SQLMode) HasTimeTruncateFractional() bool {
return m&ModeTimeTruncateFractional == ModeTimeTruncateFractional
}

// HasNoZeroDateMode detects if 'NO_ZERO_DATE' mode is set in SQLMode
func (m SQLMode) HasNoZeroDateMode() bool {
return m&ModeNoZeroDate == ModeNoZeroDate
Expand Down Expand Up @@ -483,6 +488,7 @@ const (
ModeNoEngineSubstitution
ModePadCharToFullLength
ModeAllowInvalidDates
ModeTimeTruncateFractional
ModeNone = 0
)

Expand Down Expand Up @@ -562,6 +568,7 @@ var Str2SQLMode = map[string]SQLMode{
"NO_ENGINE_SUBSTITUTION": ModeNoEngineSubstitution,
"PAD_CHAR_TO_FULL_LENGTH": ModePadCharToFullLength,
"ALLOW_INVALID_DATES": ModeAllowInvalidDates,
"TIME_TRUNCATE_FRACTIONAL": ModeTimeTruncateFractional,
}

// CombinationSQLMode is the special modes that provided as shorthand for combinations of mode values.
Expand Down
1 change: 1 addition & 0 deletions pkg/table/tables/partition.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ func NewPartitionExprBuildCtx() expression.BuildContext {
// estimate some undetermined result when locating a row to a partition.
// See issue: https://github.com/pingcap/tidb/issues/54271 for details.
exprstatic.WithSQLMode(mysql.ModeAllowInvalidDates),
// TIME will round FSP, TIME_TRUNCATE_FRACTIONAL is not considered!
exprstatic.WithTypeFlags(types.StrictFlags.
WithIgnoreTruncateErr(true).
WithIgnoreZeroDateErr(true).
Expand Down
16 changes: 16 additions & 0 deletions pkg/types/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const (
FlagSkipUTF8MB4Check
// FlagCastTimeToYearThroughConcat indicates to cast time to year through concatenation. For example, `00:19:59` will be converted to '1959'
FlagCastTimeToYearThroughConcat
// FlagTimeTruncateFractional indicates time fsp should be truncated instead of rounded.
// See TIME_TRUNCATE_FRACTIONAL SQL Mode
FlagTimeTruncateFractional
)

// AllowNegativeToUnsigned indicates whether the flag `FlagAllowNegativeToUnsigned` is set
Expand Down Expand Up @@ -196,6 +199,19 @@ func (f Flags) WithCastTimeToYearThroughConcat(flag bool) Flags {
return f &^ FlagCastTimeToYearThroughConcat
}

// TimeTruncateFractional indicates whether the flag `FlagTimeTruncateFractional` is set
func (f Flags) TimeTruncateFractional() bool {
return f&FlagTimeTruncateFractional != 0
}

// WithTimeTruncateFractional returns a new flags with `FlagTimeTruncateFractional` set/unset according to the skip parameter
func (f Flags) WithTimeTruncateFractional(set bool) Flags {
if set {
return f | FlagTimeTruncateFractional
}
return f &^ FlagTimeTruncateFractional
}

// Context provides the information when converting between different types.
type Context struct {
flags Flags
Expand Down
16 changes: 13 additions & 3 deletions pkg/types/fsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ func CheckFsp(fsp int) (int, error) {
return fsp, nil
}

// ParseFrac parses the input string according to fsp, returns the microsecond,
// ParseTimeFrac parses the input string according to fsp, returns the microsecond,
// and also a bool value to indice overflow. eg:
// "999" fsp=2 will overflow.
func ParseFrac(s string, fsp int) (v int, overflow bool, err error) {
func ParseTimeFrac(s string, fsp int, truncate bool) (v int, overflow bool, err error) {
if len(s) == 0 {
return 0, false, nil
}
Expand All @@ -69,7 +69,17 @@ func ParseFrac(s string, fsp int) (v int, overflow bool, err error) {
return
}

// Round when fsp < string length.
if truncate {
if fsp == 0 {
return 0, false, nil
}
tmp, e := strconv.ParseInt(s[:fsp], 10, 64)
if e != nil {
return 0, false, errors.Trace(e)
}
v = int(float64(tmp) * math.Pow10(MaxFsp-fsp))
return
}
tmp, e := strconv.ParseInt(s[:fsp+1], 10, 64)
if e != nil {
return 0, false, errors.Trace(e)
Expand Down
46 changes: 35 additions & 11 deletions pkg/types/fsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,62 +60,86 @@ func TestCheckFsp(t *testing.T) {
require.NoError(t, err)
}

func TestParseFrac(t *testing.T) {
obtained, overflow, err := ParseFrac("", 5)
func TestParseTimeFrac(t *testing.T) {
obtained, overflow, err := ParseTimeFrac("", 5, false)
require.Equal(t, 0, obtained)
require.False(t, overflow)
require.NoError(t, err)

a := 200
obtained, overflow, err = ParseFrac("999", int(int8(a)))
obtained, overflow, err = ParseTimeFrac("999", int(int8(a)), false)
require.Equal(t, 0, obtained)
require.False(t, overflow)
require.Error(t, err)
require.Regexp(t, "^Invalid fsp ", err.Error())

obtained, overflow, err = ParseFrac("NotNum", MaxFsp)
obtained, overflow, err = ParseTimeFrac("NotNum", MaxFsp, false)
require.Equal(t, 0, obtained)
require.False(t, overflow)
require.Error(t, err)
require.Regexp(t, "^strconv.ParseInt:", err.Error())

obtained, overflow, err = ParseFrac("1235", 6)
obtained, overflow, err = ParseTimeFrac("1235", 6, false)
require.Equal(t, 123500, obtained)
require.False(t, overflow)
require.NoError(t, err)

obtained, overflow, err = ParseFrac("123456", 4)
obtained, overflow, err = ParseTimeFrac("123456", 4, false)
require.Equal(t, 123500, obtained)
require.False(t, overflow)
require.NoError(t, err)

obtained, overflow, err = ParseFrac("1234567", 6)
obtained, overflow, err = ParseTimeFrac("1234567", 6, false)
require.Equal(t, 123457, obtained)
require.False(t, overflow)
require.NoError(t, err)

obtained, overflow, err = ParseFrac("1234567", 4)
obtained, overflow, err = ParseTimeFrac("1234567", 4, false)
require.Equal(t, 123500, obtained)
require.False(t, overflow)
require.NoError(t, err)

// 1236 round 3 -> 124 -> 124000
obtained, overflow, err = ParseFrac("1236", 3)
obtained, overflow, err = ParseTimeFrac("1236", 3, false)
require.Equal(t, 124000, obtained)
require.False(t, overflow)
require.NoError(t, err)

// 03123 round 2 -> 3 -> 30000
obtained, overflow, err = ParseFrac("0312", 2)
obtained, overflow, err = ParseTimeFrac("0312", 2, false)
require.Equal(t, 30000, obtained)
require.False(t, overflow)
require.NoError(t, err)

// 999 round 2 -> 100 -> overflow
obtained, overflow, err = ParseFrac("999", 2)
obtained, overflow, err = ParseTimeFrac("999", 2, false)
require.Equal(t, 0, obtained)
require.True(t, overflow)
require.NoError(t, err)

// 999 truncate 2 -> 990000
obtained, overflow, err = ParseTimeFrac("999", 2, true)
require.Equal(t, 990000, obtained)
require.False(t, overflow)
require.NoError(t, err)

// 999 truncate 6 -> 999000
obtained, overflow, err = ParseTimeFrac("999", 6, true)
require.Equal(t, 999000, obtained)
require.False(t, overflow)
require.NoError(t, err)

// 999 truncate 0 -> 0
obtained, overflow, err = ParseTimeFrac("999", 0, true)
require.Equal(t, 0, obtained)
require.False(t, overflow)
require.NoError(t, err)

// 9876789 truncate 6 -> 987678
obtained, overflow, err = ParseTimeFrac("9876789", 6, true)
require.Equal(t, 987678, obtained)
require.False(t, overflow)
require.NoError(t, err)
}

func TestAlignFrac(t *testing.T) {
Expand Down
Loading