From d50a4c0413a850bbd891c3a75f8bbd6001dc223a Mon Sep 17 00:00:00 2001 From: Dan Hansen Date: Thu, 28 Mar 2024 11:19:50 -0700 Subject: [PATCH] [Date] [Datetime] [Timestamp] Handle `QUARTER`, `WEEK(DAY)`, `ISOWEEK` --- internal/encoder.go | 2 +- internal/function_date.go | 137 +++++++++++++++++-- internal/function_datetime.go | 243 ++++++++++----------------------- internal/function_timestamp.go | 95 +++---------- query_test.go | 231 ++++++++++++++++++++----------- 5 files changed, 365 insertions(+), 343 deletions(-) diff --git a/internal/encoder.go b/internal/encoder.go index aa6aa16..de43699 100644 --- a/internal/encoder.go +++ b/internal/encoder.go @@ -225,7 +225,7 @@ func bytesValueFromLiteral(lit string) (BytesValue, error) { } func dateValueFromLiteral(days int64) (DateValue, error) { - t := time.Unix(int64(time.Duration(days)*24*time.Hour/time.Second), 0) + t := time.Unix(int64(time.Duration(days)*24*(time.Hour/time.Second)), 0) return DateValue(t), nil } diff --git a/internal/function_date.go b/internal/function_date.go index 8ec629d..9055deb 100644 --- a/internal/function_date.go +++ b/internal/function_date.go @@ -2,6 +2,7 @@ package internal import ( "fmt" + "strings" "time" ) @@ -65,6 +66,9 @@ func DATE_ADD(t time.Time, v int64, part string) (Value, error) { return DateValue(addMonth(t, int(v))), nil case "YEAR": return DateValue(addYear(t, int(v))), nil + case "QUARTER": + return DateValue(addMonth(t, 3)), nil + } return nil, fmt.Errorf("unexpected part value %s", part) } @@ -83,38 +87,149 @@ func DATE_SUB(t time.Time, v int64, part string) (Value, error) { return nil, fmt.Errorf("unexpected part value %s", part) } +var WeekPartToOffset = map[string]int{ + "WEEK": 0, + "WEEK_MONDAY": 1, + "WEEK_TUESDAY": 2, + "WEEK_WEDNESDAY": 3, + "WEEK_THURSDAY": 4, + "WEEK_FRIDAY": 5, + "WEEK_SATURDAY": 6, +} + func DATE_DIFF(a, b time.Time, part string) (Value, error) { + yearISOA, weekA := a.ISOWeek() + yearISOB, weekB := b.ISOWeek() + + if strings.HasPrefix(part, "WEEK") { + boundary, ok := WeekPartToOffset[part] + + if !ok { + return nil, fmt.Errorf("unsupported week date part: %s", part) + } + + isNegative := false + start, end := b, a + if b.Unix() > a.Unix() { + start, end = a, b + isNegative = true + } + + // Manually calculate the number of days based off Unix seconds + // time.Time.Sub returns "Infinite" max duration for the case of 9999-12-31.Sub(0001-01-01) + // The maximum time.Duration is ~290 years due to being represented in int64 nanosecond resolution + days := (end.Unix() - start.Unix()) / 24 / 60 / 60 + // Calculate number of complete weeks between start and end + fullWeeks := days / 7 + remainder := days % 7 + + counts := make([]int64, 7) + + for _, day := range WeekPartToOffset { + counts[day] = fullWeeks + } + + startingDay := int64(start.Weekday()) + + for remainder > 0 { + counts[(startingDay+remainder)%7]++ + remainder-- + } + + result := counts[boundary] + + if isNegative { + result = -result + } + + return IntValue(result), nil + } + + diff := a.Sub(b) + switch part { case "DAY": - return IntValue(int64(a.Sub(b).Hours() / 24)), nil - case "WEEK": - _, aWeek := a.ISOWeek() - _, bWeek := b.ISOWeek() - return IntValue(aWeek - bWeek), nil + diffDay := diff / (24 * time.Hour) + mod := diff % (24 * time.Hour) + if mod > 0 { + diffDay++ + } else if mod < 0 { + diffDay-- + } + return IntValue(diffDay), nil + case "ISOWEEK": + return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil case "MONTH": - return IntValue((a.Year() * 12 + int(a.Month())) - (b.Year() * 12 + int(b.Month()))), nil + return IntValue((a.Year()*12 + int(a.Month())) - (b.Year()*12 + int(b.Month()))), nil case "YEAR": return IntValue(a.Year() - b.Year()), nil + case "ISOYEAR": + return IntValue(yearISOA - yearISOB), nil } return nil, fmt.Errorf("unexpected part value %s", part) } +var quarterStartMonths = []time.Month{time.January, time.April, time.July, time.October} + func DATE_TRUNC(t time.Time, part string) (Value, error) { + yearISO, weekISO := t.ISOWeek() + + if strings.HasPrefix(part, "WEEK") { + startOfWeek, ok := WeekPartToOffset[part] + if !ok { + return nil, fmt.Errorf("unknown week part: %s", part) + } + + for int(t.Weekday()) != startOfWeek { + t = t.AddDate(0, 0, -1) + } + + return DateValue(t), nil + } + switch part { case "DAY": return DateValue(time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())), nil case "ISOWEEK": - return nil, fmt.Errorf("currently unsupported DATE_TRUNC with ISO_WEEK") - case "WEEK": - return DateValue(t.AddDate(0, 0, -int(t.Weekday()))), nil + return DateValue(time.Date( + yearISO, + 0, + 7*weekISO, + 0, + 0, + 0, + 0, + t.Location(), + )), nil case "MONTH": return DateValue(time.Time{}.AddDate(t.Year()-1, int(t.Month())-1, 0)), nil case "QUARTER": - return nil, fmt.Errorf("currently unsupported DATE_TRUNC with QUARTER") + return DateValue( // 1, 4, 7, 10 + time.Date( + t.Year(), + quarterStartMonths[int64((t.Month()-1)/3)], + 1, + 0, + 0, + 0, + 0, + t.Location(), + ), + ), nil case "YEAR": return DateValue(time.Time{}.AddDate(t.Year()-1, 0, 0)), nil case "ISOYEAR": - return nil, fmt.Errorf("currently unsupported DATE_TRUNC with ISO_YAER") + firstDay := time.Date( + yearISO, + 1, + 1, + 0, + 0, + 0, + 0, + t.Location(), + ) + return DateValue(firstDay.AddDate(0, 0, 1-int(firstDay.Weekday()))), nil } return nil, fmt.Errorf("unexpected part value %s", part) } diff --git a/internal/function_datetime.go b/internal/function_datetime.go index 95fac0a..2a8da2d 100644 --- a/internal/function_datetime.go +++ b/internal/function_datetime.go @@ -117,18 +117,28 @@ func DATETIME_ADD(t time.Time, v int64, part string) (Value, error) { return DatetimeValue(t.Add(time.Duration(v) * time.Minute)), nil case "HOUR": return DatetimeValue(t.Add(time.Duration(v) * time.Hour)), nil - case "DAY": - return DatetimeValue(t.AddDate(0, 0, int(v))), nil - case "WEEK": - return DatetimeValue(t.AddDate(0, 0, int(v*7))), nil - case "MONTH": - return DatetimeValue(addMonth(t, int(v))), nil - case "QUARTER": - return DatetimeValue(addMonth(t, 3*int(v))), nil - case "YEAR": - return DatetimeValue(addYear(t, int(v))), nil + default: + date, err := DATE_ADD(t, v, part) + if err != nil { + return nil, fmt.Errorf("DATETIME_ADD: %w", err) + } + datetime, err := date.ToTime() + if err != nil { + return nil, fmt.Errorf("DATETIME_ADD: %w", err) + } + return DatetimeValue( + time.Date( + datetime.Year(), + datetime.Month(), + datetime.Day(), + t.Hour(), + t.Minute(), + t.Second(), + t.Nanosecond(), + t.Location(), + ), + ), nil } - return nil, fmt.Errorf("DATETIME_ADD: unexpected part value %s", part) } func DATETIME_SUB(t time.Time, v int64, part string) (Value, error) { @@ -143,24 +153,32 @@ func DATETIME_SUB(t time.Time, v int64, part string) (Value, error) { return DatetimeValue(t.Add(-time.Duration(v) * time.Minute)), nil case "HOUR": return DatetimeValue(t.Add(-time.Duration(v) * time.Hour)), nil - case "DAY": - return DatetimeValue(t.AddDate(0, 0, -int(v))), nil - case "WEEK": - return DatetimeValue(t.AddDate(0, 0, -int(v*7))), nil - case "MONTH": - return DatetimeValue(addMonth(t, -int(v))), nil - case "QUARTER": - return DatetimeValue(addMonth(t, -3*int(v))), nil - case "YEAR": - return DatetimeValue(addYear(t, -int(v))), nil + default: + date, err := DATE_SUB(t, v, part) + if err != nil { + return nil, fmt.Errorf("DATETIME_SUB: %w", err) + } + datetime, err := date.ToTime() + if err != nil { + return nil, fmt.Errorf("DATETIME_SUB: %w", err) + } + return DatetimeValue( + time.Date( + datetime.Year(), + datetime.Month(), + datetime.Day(), + t.Hour(), + t.Minute(), + t.Second(), + t.Nanosecond(), + t.Location(), + ), + ), nil } - return nil, fmt.Errorf("DATETIME_SUB: unexpected part value %s", part) } func DATETIME_DIFF(a, b time.Time, part string) (Value, error) { diff := a.Sub(b) - yearISOA, weekA := a.ISOWeek() - yearISOB, weekB := b.ISOWeek() switch part { case "MICROSECOND": return IntValue(diff / time.Microsecond), nil @@ -172,87 +190,16 @@ func DATETIME_DIFF(a, b time.Time, part string) (Value, error) { return IntValue(diff / time.Minute), nil case "HOUR": return IntValue(diff / time.Hour), nil - case "DAY": - diffDay := diff / (24 * time.Hour) - mod := diff % (24 * time.Hour) - if mod > 0 { - diffDay++ - } else if mod < 0 { - diffDay-- - } - return IntValue(diffDay), nil - case "WEEK": - if a.Weekday() > 0 { - weekA-- - } - if b.Weekday() > 0 { - weekB-- - } - return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil - case "WEEK_MONDAY": - if a.Weekday() > 1 { - weekA-- - } - if b.Weekday() > 1 { - weekB-- - } - return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil - case "WEEK_TUESDAY": - if a.Weekday() > 2 { - weekA-- - } - if b.Weekday() > 2 { - weekB-- - } - return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil - case "WEEK_WEDNESDAY": - if a.Weekday() > 3 { - weekA-- - } - if b.Weekday() > 3 { - weekB-- - } - return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil - case "WEEK_THURSDAY": - if a.Weekday() > 4 { - weekA-- - } - if b.Weekday() > 4 { - weekB-- - } - return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil - case "WEEK_FRIDAY": - if a.Weekday() > 5 { - weekA-- - } - if b.Weekday() > 5 { - weekB-- - } - return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil - case "WEEK_SATURDAY": - if a.Weekday() > 6 { - weekA-- - } - if b.Weekday() > 6 { - weekB-- - } - return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil - case "ISOWEEK": - return IntValue((a.Year()-b.Year())*48 + weekA - weekB), nil - case "MONTH": - return IntValue((a.Year()-b.Year())*12 + int(a.Month()) - int(b.Month())), nil - case "QUARTER": - return IntValue(a.Month()/4 - b.Month()/4), nil - case "YEAR": - return IntValue(a.Year() - b.Year()), nil - case "ISOYEAR": - return IntValue(yearISOA - yearISOB), nil } - return nil, fmt.Errorf("DATETIME_DIFF: unexpected part value %s", part) + + value, err := DATE_DIFF(a, b, part) + if err != nil { + return nil, fmt.Errorf("DATETIME_DIFF: %w", err) + } + return value, nil } func DATETIME_TRUNC(t time.Time, part string) (Value, error) { - yearISO, weekISO := t.ISOWeek() switch part { case "MICROSECOND": return DatetimeValue(t), nil @@ -302,80 +249,28 @@ func DATETIME_TRUNC(t time.Time, part string) (Value, error) { 0, t.Location(), )), nil - case "DAY": - return DatetimeValue(time.Date( - t.Year(), - t.Month(), - t.Day(), - 0, - 0, - 0, - 0, - t.Location(), - )), nil - case "WEEK": - return DatetimeValue(t.AddDate(0, 0, int(t.Weekday()))), nil - case "WEEK_MONDAY": - return DatetimeValue(t.AddDate(0, 0, int(t.Weekday())-6)), nil - case "WEEK_TUESDAY": - return DatetimeValue(t.AddDate(0, 0, int(t.Weekday())-5)), nil - case "WEEK_WEDNESDAY": - return DatetimeValue(t.AddDate(0, 0, int(t.Weekday())-4)), nil - case "WEEK_THURSDAY": - return DatetimeValue(t.AddDate(0, 0, int(t.Weekday())-3)), nil - case "WEEK_FRIDAY": - return DatetimeValue(t.AddDate(0, 0, int(t.Weekday())-2)), nil - case "WEEK_SATURDAY": - return DatetimeValue(t.AddDate(0, 0, int(t.Weekday())-1)), nil - case "ISOWEEK": - return DatetimeValue(time.Date( - yearISO, - 0, - 7*weekISO, - 0, - 0, - 0, - 0, - t.Location(), - )), nil - case "MONTH": - return DatetimeValue(time.Date( - t.Year(), - t.Month(), - 0, - 0, - 0, - 0, - 0, - t.Location(), - )), nil - case "QUARTER": - return nil, fmt.Errorf("currently unsupported DATE_TRUNC with QUARTER") - case "YEAR": - return DatetimeValue(time.Date( - t.Year(), - 1, - 1, - 0, - 0, - 0, - 0, - t.Location(), - )), nil - case "ISOYEAR": - firstDay := time.Date( - yearISO, - 1, - 1, - 0, - 0, - 0, - 0, - t.Location(), - ) - return DatetimeValue(firstDay.AddDate(0, 0, 1-int(firstDay.Weekday()))), nil + default: + date, err := DATE_TRUNC(t, part) + if err != nil { + return nil, fmt.Errorf("DATETIME_TRUNC: %w", err) + } + datetime, err := date.ToTime() + if err != nil { + return nil, fmt.Errorf("DATETIME_TRUNC: %w", err) + } + return DatetimeValue( + time.Date( + datetime.Year(), + datetime.Month(), + datetime.Day(), + datetime.Hour(), + datetime.Minute(), + datetime.Second(), + datetime.Nanosecond(), + datetime.Location(), + ), + ), nil } - return nil, fmt.Errorf("unexpected part value %s", part) } func FORMAT_DATETIME(format string, t time.Time) (Value, error) { diff --git a/internal/function_timestamp.go b/internal/function_timestamp.go index a709adf..c851dad 100644 --- a/internal/function_timestamp.go +++ b/internal/function_timestamp.go @@ -104,17 +104,14 @@ func TIMESTAMP_DIFF(a, b time.Time, part string) (Value, error) { return IntValue(diff / time.Minute), nil case "HOUR": return IntValue(diff / time.Hour), nil - case "DAY": - diffDay := diff / (24 * time.Hour) - mod := diff % (24 * time.Hour) - if mod > 0 { - diffDay++ - } else if mod < 0 { - diffDay-- + default: + dateDiff, err := DATE_DIFF(a, b, part) + if err != nil { + return nil, fmt.Errorf("TIMESTAMP_DIFF: %w", err) } - return IntValue(diffDay), nil + + return dateDiff, nil } - return nil, nil } func TIMESTAMP_TRUNC(t time.Time, part, zone string) (Value, error) { @@ -123,7 +120,7 @@ func TIMESTAMP_TRUNC(t time.Time, part, zone string) (Value, error) { return nil, err } t = t.In(loc) - yearISO, weekISO := t.ISOWeek() + switch part { case "MICROSECOND": return TimestampValue(t), nil @@ -173,80 +170,26 @@ func TIMESTAMP_TRUNC(t time.Time, part, zone string) (Value, error) { 0, loc, )), nil - case "DAY": + default: + date, err := DATE_TRUNC(t, part) + if err != nil { + return nil, fmt.Errorf("TIMESTAMP_TRUNC: %w", err) + } + dateTime, err := date.ToTime() + if err != nil { + return nil, fmt.Errorf("TIMESTAMP_TRUNC: %w", err) + } return TimestampValue(time.Date( - t.Year(), - t.Month(), - t.Day(), + dateTime.Year(), + dateTime.Month(), + dateTime.Day(), 0, 0, 0, 0, loc, )), nil - case "WEEK": - return TimestampValue(t.AddDate(0, 0, int(t.Weekday()))), nil - case "WEEK_MONDAY": - return TimestampValue(t.AddDate(0, 0, int(t.Weekday())-6)), nil - case "WEEK_TUESDAY": - return TimestampValue(t.AddDate(0, 0, int(t.Weekday())-5)), nil - case "WEEK_WEDNESDAY": - return TimestampValue(t.AddDate(0, 0, int(t.Weekday())-4)), nil - case "WEEK_THURSDAY": - return TimestampValue(t.AddDate(0, 0, int(t.Weekday())-3)), nil - case "WEEK_FRIDAY": - return TimestampValue(t.AddDate(0, 0, int(t.Weekday())-2)), nil - case "WEEK_SATURDAY": - return TimestampValue(t.AddDate(0, 0, int(t.Weekday())-1)), nil - case "ISOWEEK": - return TimestampValue(time.Date( - yearISO, - 0, - 7*weekISO, - 0, - 0, - 0, - 0, - t.Location(), - )), nil - case "MONTH": - return TimestampValue(time.Date( - t.Year(), - t.Month(), - 0, - 0, - 0, - 0, - 0, - t.Location(), - )), nil - case "QUARTER": - return nil, fmt.Errorf("TIMESTAMP_TRUNC: unimplemented QUARTER") - case "YEAR": - return TimestampValue(time.Date( - t.Year(), - 1, - 1, - 0, - 0, - 0, - 0, - t.Location(), - )), nil - case "ISOYEAR": - firstDay := time.Date( - yearISO, - 1, - 1, - 0, - 0, - 0, - 0, - t.Location(), - ) - return TimestampValue(firstDay.AddDate(0, 0, 1-int(firstDay.Weekday()))), nil } - return nil, fmt.Errorf("TIMESTAMP_TRUNC: unexpected part value %s", part) } func FORMAT_TIMESTAMP(format string, t time.Time, zone string) (Value, error) { diff --git a/query_test.go b/query_test.go index f92065e..3689920 100644 --- a/query_test.go +++ b/query_test.go @@ -4089,6 +4089,46 @@ WITH example AS ( query: `SELECT DATE_ADD('2023-01-29', INTERVAL 1 MONTH)`, expectedRows: [][]interface{}{{"2023-02-28"}}, }, + { + name: "date_add quarter", + query: `SELECT DATE_ADD('2023-01-01', INTERVAL 1 QUARTER), DATE_ADD('2023-11-30', INTERVAL 1 QUARTER)`, + expectedRows: [][]interface{}{{"2023-04-01", "2024-02-29"}}, + }, + { + name: "date_trunc with quarter", + query: `SELECT DATE_TRUNC(DATE "2017-01-05", QUARTER), DATE_TRUNC(DATE "2017-02-05", QUARTER), DATE_TRUNC(DATE "2017-08-05", QUARTER), DATE_TRUNC(DATE "2017-11-05", QUARTER), DATE_TRUNC(DATE "2017-12-31", QUARTER)`, + expectedRows: [][]interface{}{{"2017-01-01", "2017-01-01", "2017-07-01", "2017-10-01", "2017-10-01"}}, + }, + + { + name: "datetime_trunc with quarter", + query: `SELECT DATETIME_TRUNC(DATETIME "2017-01-05", QUARTER), DATETIME_TRUNC(DATETIME "2017-02-05", QUARTER), DATETIME_TRUNC(DATETIME "2017-08-05", QUARTER), DATETIME_TRUNC(DATETIME "2017-11-05", QUARTER), DATETIME_TRUNC(DATETIME "2017-12-31", QUARTER)`, + expectedRows: [][]interface{}{{"2017-01-01T00:00:00", "2017-01-01T00:00:00", "2017-07-01T00:00:00", "2017-10-01T00:00:00", "2017-10-01T00:00:00"}}, + }, + { + name: "timestamp_trunc with quarter", + query: `SELECT TIMESTAMP_TRUNC(TIMESTAMP "2017-01-05", QUARTER, "Pacific/Auckland"), TIMESTAMP_TRUNC(TIMESTAMP "2017-02-05", QUARTER), TIMESTAMP_TRUNC(TIMESTAMP "2024-02-29", QUARTER), TIMESTAMP_TRUNC(TIMESTAMP "2017-08-05", QUARTER), TIMESTAMP_TRUNC(TIMESTAMP "2017-12-31", QUARTER)`, + expectedRows: [][]interface{}{{ + createTimestampFormatFromString("2016-12-31 11:00:00+00"), + createTimestampFormatFromString("2017-01-01 00:00:00+00"), + createTimestampFormatFromString("2024-01-01 00:00:00+00"), + createTimestampFormatFromString("2017-07-01 00:00:00+00"), + createTimestampFormatFromString("2017-10-01 00:00:00+00"), + }}, + }, + { + name: "datetime_trunc with day weekday", + query: `SELECT DATETIME_TRUNC(DATETIME "2024-03-29", WEEK(MONDAY))`, + expectedRows: [][]interface{}{{"2024-03-25T00:00:00"}}, + }, + { + name: "datetime_trunc isoyear", + query: `SELECT + DATETIME_TRUNC('2015-06-15', ISOYEAR) AS isoyear_boundary, + EXTRACT(ISOYEAR FROM DATE '2015-06-15') AS isoyear_number; +`, + expectedRows: [][]interface{}{{"2014-12-29T00:00:00", int64(2015)}}, + }, { name: "date_sub", query: `SELECT DATE_SUB('2023-03-31', INTERVAL 1 MONTH)`, @@ -4109,87 +4149,106 @@ WITH example AS ( }, }, { - name: "base date is epoch julian", - query: `SELECT PARSE_DATE("%j", "001")`, - expectedRows: [][]interface{}{ - {"1970-01-01"}, - }, - }, - { - name: "base datetime is epoch julian", - query: `SELECT PARSE_DATETIME("%j", "001")`, - expectedRows: [][]interface{}{ - {"1970-01-01T00:00:00"}, - }, - }, - { - name: "base date is epoch julian different day", - query: `SELECT PARSE_DATE("%j", "002")`, - expectedRows: [][]interface{}{ - {"1970-01-02"}, - }, - }, - { - name: "parse date with two digit year and julian day", - query: `SELECT PARSE_DATE("%y%j", "70002")`, - expectedRows: [][]interface{}{ - {"1970-01-02"}, - }, - }, - { - name: "parse date with two digit year before 2000 and julian day", - query: `SELECT PARSE_DATE("%y%j", "95033")`, - expectedRows: [][]interface{}{ - {"1995-02-02"}, - }, + name: "extract date", + query: ` +SELECT date, EXTRACT(ISOYEAR FROM date), EXTRACT(YEAR FROM date), EXTRACT(MONTH FROM date), + EXTRACT(ISOWEEK FROM date), EXTRACT(WEEK FROM date), EXTRACT(DAY FROM date) FROM UNNEST([DATE '2015-12-23']) AS date`, + expectedRows: [][]interface{}{{"2015-12-23", int64(2015), int64(2015), int64(12), int64(52), int64(51), int64(23)}}, }, { - name: "parse datetime with two digit year before 2000 and julian day", - query: `SELECT PARSE_DATETIME("%y%j%H%M%S", "95033101010")`, - expectedRows: [][]interface{}{ - {"1995-02-02T10:10:10"}, - }, + name: "date_diff with week", + query: `SELECT DATE_DIFF(DATE '2017-10-17', DATE '2017-10-12', WEEK) AS weeks_diff`, + expectedRows: [][]interface{}{{int64(1)}}, }, { - name: "parse date with two digit year after 2000 and julian day", - query: `SELECT PARSE_DATE("%y%j", "22120")`, - expectedRows: [][]interface{}{ - {"2022-04-30"}, - }, + name: "date_diff with week day", + query: `SELECT DATE_DIFF(DATE '2024-03-19', DATE '2024-03-24', WEEK(MONDAY))`, + expectedRows: [][]interface{}{{ + // No Mondays occurred between 2024-03-24 abd 2024-03-19 + int64(0), + }}, }, { - name: "parse datetime with two digit year after 2000 and julian day", - query: `SELECT PARSE_DATETIME("%y%j-%H:%M:%S", "22120-10:10:10")`, - expectedRows: [][]interface{}{ - {"2022-04-30T10:10:10"}, - }, + name: "date_diff with week day", + query: `SELECT + DATE_DIFF(DATE '2024-03-19', DATE '2024-03-24', WEEK(SUNDAY)), + DATE_DIFF(DATE '2024-03-19', DATE '2024-03-24', WEEK(MONDAY)), + DATE_DIFF(DATE '2024-03-25', DATE '2024-03-19', WEEK(SUNDAY)), + DATE_DIFF(DATE '2024-03-19', DATE '2024-03-25', WEEK(MONDAY)), + DATE_DIFF(DATE '2024-03-19', DATE '2017-10-25', WEEK(MONDAY)), + DATE_DIFF('0001-01-01', '9999-12-31', WEEK(SUNDAY))`, + expectedRows: [][]interface{}{{ + // 1 Sunday occurred between 2024-03-19 and 2024-03-24 + int64(-1), + // No Mondays occurred between 2024-03-24 abd 2024-03-19 + int64(0), + // 1 Monday occurred between 2024-03-25 and 2024-03-19 + int64(1), + // -1 Monday occurred between 2024-03-19 and 2024-03-25 + int64(-1), + int64(334), + int64(-521722), + }}, }, { - name: "parse date with two digit year after 2000 and julian day leap year", - query: `SELECT PARSE_DATE("%y%j", "24120")`, - expectedRows: [][]interface{}{ - {"2024-04-29"}, - }, + name: "datetime_diff with week day", + query: `SELECT + DATETIME_DIFF(DATETIME '2024-03-19', DATETIME '2024-03-24', WEEK(SUNDAY)), + DATETIME_DIFF(DATETIME '2024-03-19', DATETIME '2024-03-24', WEEK(MONDAY)), + DATETIME_DIFF(DATETIME '2024-03-25', DATETIME '2024-03-19', WEEK(SUNDAY)), + DATETIME_DIFF(DATETIME '2024-03-19', DATETIME '2024-03-25', WEEK(MONDAY)), + DATETIME_DIFF(DATETIME '2024-03-19', DATETIME '2017-10-25', WEEK(MONDAY)), + DATETIME_DIFF(DATETIME '2024-02-21', DATETIME '2024-02-29', WEEK(MONDAY))`, + expectedRows: [][]interface{}{{ + // 1 Sunday occurred between 2024-03-19 and 2024-03-24 + int64(-1), + // No Mondays occurred between 2024-03-24 abd 2024-03-19 + int64(0), + // 1 Monday occurred between 2024-03-25 and 2024-03-19 + int64(1), + // -1 Monday occurred between 2024-03-19 and 2024-03-25 + int64(-1), + int64(334), + int64(-1), + }}, }, { - name: "parse datetime with two digit year after 2000 and julian day leap year", - query: `SELECT PARSE_DATETIME("%y%j %H:%M", "24120 02:04")`, - expectedRows: [][]interface{}{ - {"2024-04-29T02:04:00"}, - }, + name: "datetime_diff with week day 1 week", + query: `SELECT + DATETIME_DIFF(DATETIME '2024-02-21', DATETIME '2024-02-29', WEEK(MONDAY))`, + expectedRows: [][]interface{}{{ + int64(-1), + }}, }, { - name: "extract date", - query: ` -SELECT date, EXTRACT(ISOYEAR FROM date), EXTRACT(YEAR FROM date), EXTRACT(MONTH FROM date), - EXTRACT(ISOWEEK FROM date), EXTRACT(WEEK FROM date), EXTRACT(DAY FROM date) FROM UNNEST([DATE '2015-12-23']) AS date`, - expectedRows: [][]interface{}{{"2015-12-23", int64(2015), int64(2015), int64(12), int64(52), int64(51), int64(23)}}, + name: "datetime_diff with week day", + query: `SELECT DATETIME_DIFF(DATETIME '2024-03-19', DATETIME '2024-03-25', WEEK(MONDAY))`, + expectedRows: [][]interface{}{{ + // -1 Monday occurred between 2024-03-19 and 2024-03-25 + int64(-1), + }}, }, { - name: "date_diff with week", - query: `SELECT DATE_DIFF(DATE '2017-10-17', DATE '2017-10-12', WEEK) AS weeks_diff`, - expectedRows: [][]interface{}{{int64(1)}}, + name: "timestamp diff with week day", + query: `SELECT + TIMESTAMP_DIFF(DATETIME '2024-03-19', DATETIME '2024-03-24', WEEK(SUNDAY)), + TIMESTAMP_DIFF(DATETIME '2024-03-19', DATETIME '2024-03-24', WEEK(MONDAY)), + TIMESTAMP_DIFF(DATETIME '2024-03-25', DATETIME '2024-03-19', WEEK(SUNDAY)), + TIMESTAMP_DIFF(DATETIME '2024-03-19', DATETIME '2024-03-25', WEEK(MONDAY)), + TIMESTAMP_DIFF(DATETIME '2024-03-19', DATETIME '2017-10-25', WEEK(MONDAY))`, + expectedRows: [][]interface{}{{ + // 1 Sunday occurred between 2024-03-19 and 2024-03-24 + int64(-1), + // No Mondays occurred between 2024-03-24 abd 2024-03-19 + int64(0), + // 1 Monday occurred between 2024-03-25 and 2024-03-19 + int64(1), + // -1 Monday occurred between 2024-03-19 and 2024-03-25 + int64(-1), + int64(334), + }}, }, + { name: "date_diff with month", query: `SELECT DATE_DIFF(DATE '2018-01-01', DATE '2017-10-30', MONTH) AS months_diff`, @@ -4439,7 +4498,7 @@ SELECT date, EXTRACT(ISOYEAR FROM date), EXTRACT(YEAR FROM date), EXTRACT(MONTH expectedRows: [][]interface{}{{int64(1)}}, }, { - name: "datetime_diff with year", + name: "datetime_diff with year, ISOYEAR", query: `SELECT DATETIME_DIFF('2017-12-30 00:00:00', '2014-12-30 00:00:00', YEAR), DATETIME_DIFF('2017-12-30 00:00:00', '2014-12-30 00:00:00', ISOYEAR)`, expectedRows: [][]interface{}{{int64(3), int64(2)}}, }, @@ -4649,6 +4708,16 @@ SELECT date, EXTRACT(ISOYEAR FROM date), EXTRACT(YEAR FROM date), EXTRACT(MONTH {createTimestampFormatFromTime(now.UTC())}, }, }, + + { + name: "minimum / maximum date value", + query: `SELECT DATE '0001-01-01', DATE '9999-12-31'`, + expectedRows: [][]interface{}{ + { + "0001-01-01", "9999-12-31", + }, + }, + }, { name: "minimum / maximum timestamp value uses microsecond precision and range", query: `SELECT TIMESTAMP '0001-01-01 00:00:00.000000+00', TIMESTAMP '9999-12-31 23:59:59.999999+00'`, @@ -4711,20 +4780,20 @@ SELECT date, EXTRACT(ISOYEAR FROM date), EXTRACT(YEAR FROM date), EXTRACT(MONTH {createTimestampFormatFromString("2008-12-25 00:00:00+00"), createTimestampFormatFromString("2008-12-25 08:00:00+00")}, }, }, - // { - // name: "timestamp_trunc with week", - // query: `SELECT timestamp_value AS timestamp_value, - // TIMESTAMP_TRUNC(timestamp_value, WEEK(MONDAY), "UTC"), - // TIMESTAMP_TRUNC(timestamp_value, WEEK(MONDAY), "Pacific/Auckland") - // FROM (SELECT TIMESTAMP("2017-11-06 00:00:00+12") AS timestamp_value)`, - // expectedRows: [][]interface{}{ - // { - // createTimestampFormatFromString("2017-11-05 12:00:00+00"), - // createTimestampFormatFromString("2017-10-30 00:00:00+00"), - // createTimestampFormatFromString("2017-11-05 11:00:00+00"), - // }, - // }, - // }, + { + name: "timestamp_trunc with week", + query: `SELECT timestamp_value AS timestamp_value, + TIMESTAMP_TRUNC(timestamp_value, WEEK(MONDAY), "UTC"), + TIMESTAMP_TRUNC(timestamp_value, WEEK(MONDAY), "Pacific/Auckland") + FROM (SELECT TIMESTAMP("2017-11-06 00:00:00+12") AS timestamp_value)`, + expectedRows: [][]interface{}{ + { + createTimestampFormatFromString("2017-11-05 12:00:00+00"), + createTimestampFormatFromString("2017-10-30 00:00:00+00"), + createTimestampFormatFromString("2017-11-05 11:00:00+00"), + }, + }, + }, { name: "timestamp_trunc with year", query: `SELECT TIMESTAMP_TRUNC("2015-06-15 00:00:00+00", ISOYEAR)`,