Skip to content

Commit

Permalink
sql: add precision support for TIME/TIMETZ
Browse files Browse the repository at this point in the history
Adding support for precision for both TIME and TIMETZ. This also
includes threading through some extra parsing syntax for TIMETZ.

ALTER TABLE between TIME and TIMETZ not supported as they have different
representations.

Release note (sql change): This PR adds new support for precision for
TIME types (e.g. TIME(3) will truncate to milliseconds). Previously this
would raise syntax errors.
  • Loading branch information
otan committed Nov 21, 2019
1 parent 7d23b27 commit f769516
Show file tree
Hide file tree
Showing 23 changed files with 491 additions and 165 deletions.
3 changes: 3 additions & 0 deletions docs/generated/sql/bnf/stmt_block.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -2089,7 +2089,10 @@ character_without_length ::=

const_datetime ::=
'DATE'
| 'TIME' opt_timezone
| 'TIME' '(' iconst32 ')' opt_timezone
| 'TIMETZ'
| 'TIMETZ' '(' iconst32 ')'
| 'TIMESTAMP' opt_timezone
| 'TIMESTAMP' '(' iconst32 ')' opt_timezone
| 'TIMESTAMPTZ'
Expand Down
2 changes: 1 addition & 1 deletion pkg/ccl/changefeedccl/avro.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func columnDescToAvroSchema(colDesc *sqlbase.ColumnDescriptor) (*avroSchemaField
return d.(*tree.DTimeTZ).TimeTZ.String(), nil
}
schema.decodeFn = func(x interface{}) (tree.Datum, error) {
return tree.ParseDTimeTZ(nil, x.(string))
return tree.ParseDTimeTZ(nil, x.(string), time.Microsecond)
}
case types.TimestampFamily:
avroType = avroLogicalType{
Expand Down
63 changes: 63 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/time
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,66 @@ SELECT extract('day' from time '12:00:00')

query error pgcode 22023 extract\(\): unsupported timespan: day
SELECT extract('DAY' from time '12:00:00')

subtest precision_tests

query error precision 7 out of range
select '1:00:00.001':::TIME(7)

statement ok
CREATE TABLE time_precision_test (
id integer PRIMARY KEY,
t TIME(5)
)

statement ok
INSERT INTO time_precision_test VALUES
(1,'12:00:00.123456+03:00'),
(2,'12:00:00.12345+03:00'),
(3,'12:00:00.1234+03:00'),
(4,'12:00:00.123+03:00'),
(5,'12:00:00.12+03:00'),
(6,'12:00:00.1+03:00'),
(7,'12:00:00+03:00')

query IT
SELECT * FROM time_precision_test ORDER BY id ASC
----
1 0000-01-01 12:00:00.12346 +0000 UTC
2 0000-01-01 12:00:00.12345 +0000 UTC
3 0000-01-01 12:00:00.1234 +0000 UTC
4 0000-01-01 12:00:00.123 +0000 UTC
5 0000-01-01 12:00:00.12 +0000 UTC
6 0000-01-01 12:00:00.1 +0000 UTC
7 0000-01-01 12:00:00 +0000 UTC

query TT
select column_name, data_type FROM [SHOW COLUMNS FROM time_precision_test] ORDER BY column_name
----
id INT8
t TIME(5)

statement ok
ALTER TABLE time_precision_test ALTER COLUMN t TYPE time(6)

statement ok
INSERT INTO time_precision_test VALUES
(100,'12:00:00.123456+03:00')

query IT
SELECT * FROM time_precision_test ORDER BY id ASC
----
1 0000-01-01 12:00:00.12346 +0000 UTC
2 0000-01-01 12:00:00.12345 +0000 UTC
3 0000-01-01 12:00:00.1234 +0000 UTC
4 0000-01-01 12:00:00.123 +0000 UTC
5 0000-01-01 12:00:00.12 +0000 UTC
6 0000-01-01 12:00:00.1 +0000 UTC
7 0000-01-01 12:00:00 +0000 UTC
100 0000-01-01 12:00:00.123456 +0000 UTC

query TT
select column_name, data_type FROM [SHOW COLUMNS FROM time_precision_test] ORDER BY column_name
----
id INT8
t TIME(6)
63 changes: 63 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/timetz
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,66 @@ SELECT '2001-01-01 11:00+04:00'::timestamptz::timetz

statement ok
SET TIME ZONE UTC

subtest precision_tests

query error precision 7 out of range
select '1:00:00.001':::TIMETZ(7)

statement ok
CREATE TABLE timetz_precision_test (
id integer PRIMARY KEY,
t TIMETZ(5)
)

statement ok
INSERT INTO timetz_precision_test VALUES
(1,'12:00:00.123456+03:00'),
(2,'12:00:00.12345+03:00'),
(3,'12:00:00.1234+03:00'),
(4,'12:00:00.123+03:00'),
(5,'12:00:00.12+03:00'),
(6,'12:00:00.1+03:00'),
(7,'12:00:00+03:00')

query IT
SELECT * FROM timetz_precision_test ORDER BY id ASC
----
1 0000-01-01 12:00:00.12346 +0300 +0300
2 0000-01-01 12:00:00.12345 +0300 +0300
3 0000-01-01 12:00:00.1234 +0300 +0300
4 0000-01-01 12:00:00.123 +0300 +0300
5 0000-01-01 12:00:00.12 +0300 +0300
6 0000-01-01 12:00:00.1 +0300 +0300
7 0000-01-01 12:00:00 +0300 +0300

query TT
select column_name, data_type FROM [SHOW COLUMNS FROM timetz_precision_test] ORDER BY column_name
----
id INT8
t TIMETZ(5)

statement ok
ALTER TABLE timetz_precision_test ALTER COLUMN t TYPE timetz(6)

statement ok
INSERT INTO timetz_precision_test VALUES
(100,'12:00:00.123456+03:00')

query IT
SELECT * FROM timetz_precision_test ORDER BY id ASC
----
1 0000-01-01 12:00:00.12346 +0300 +0300
2 0000-01-01 12:00:00.12345 +0300 +0300
3 0000-01-01 12:00:00.1234 +0300 +0300
4 0000-01-01 12:00:00.123 +0300 +0300
5 0000-01-01 12:00:00.12 +0300 +0300
6 0000-01-01 12:00:00.1 +0300 +0300
7 0000-01-01 12:00:00 +0300 +0300
100 0000-01-01 12:00:00.123456 +0300 +0300

query TT
select column_name, data_type FROM [SHOW COLUMNS FROM timetz_precision_test] ORDER BY column_name
----
id INT8
t TIMETZ(6)
18 changes: 11 additions & 7 deletions pkg/sql/parser/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ func TestParse(t *testing.T) {
{`CREATE TABLE a (b SERIAL8)`},
{`CREATE TABLE a (b TIME)`},
{`CREATE TABLE a (b TIMETZ)`},
{`CREATE TABLE a (b TIME(3))`},
{`CREATE TABLE a (b TIMETZ(3))`},
{`CREATE TABLE a (b UUID)`},
{`CREATE TABLE a (b INET)`},
{`CREATE TABLE a (b "char")`},
Expand Down Expand Up @@ -1458,11 +1460,20 @@ func TestParse2(t *testing.T) {
{`SELECT CAST(1 AS "_int8")`, `SELECT CAST(1 AS INT8[])`},
{`SELECT SERIAL8 'foo', 'foo'::SERIAL8`, `SELECT INT8 'foo', 'foo'::INT8`},

{`SELECT 'a'::TIMESTAMP(3)`, `SELECT 'a'::TIMESTAMP(3)`},
{`SELECT 'a'::TIMESTAMP(3) WITHOUT TIME ZONE`, `SELECT 'a'::TIMESTAMP(3)`},
{`SELECT 'a'::TIMESTAMPTZ(3)`, `SELECT 'a'::TIMESTAMPTZ(3)`},
{`SELECT 'a'::TIMESTAMP(3) WITH TIME ZONE`, `SELECT 'a'::TIMESTAMPTZ(3)`},
{`SELECT TIMESTAMP(3) 'a'`, `SELECT TIMESTAMP(3) 'a'`},
{`SELECT TIMESTAMPTZ(3) 'a'`, `SELECT TIMESTAMPTZ(3) 'a'`},

{`SELECT 'a'::TIME(3)`, `SELECT 'a'::TIME(3)`},
{`SELECT 'a'::TIME(3) WITHOUT TIME ZONE`, `SELECT 'a'::TIME(3)`},
{`SELECT 'a'::TIMETZ(3)`, `SELECT 'a'::TIMETZ(3)`},
{`SELECT 'a'::TIME(3) WITH TIME ZONE`, `SELECT 'a'::TIMETZ(3)`},
{`SELECT TIME(3) 'a'`, `SELECT TIME(3) 'a'`},
{`SELECT TIMETZ(3) 'a'`, `SELECT TIMETZ(3) 'a'`},

{`SELECT 'a' FROM t@{FORCE_INDEX=bar}`, `SELECT 'a' FROM t@bar`},
{`SELECT 'a' FROM t@{ASC,FORCE_INDEX=idx}`, `SELECT 'a' FROM t@{FORCE_INDEX=idx,ASC}`},

Expand Down Expand Up @@ -3113,13 +3124,6 @@ func TestUnimplementedSyntax(t *testing.T) {
{`SELECT 'a'::INTERVAL SECOND(123)`, 32564, `interval second`},
{`SELECT INTERVAL(3) 'a'`, 32564, ``},

{`SELECT 'a'::TIME(123)`, 32565, ``},
{`SELECT 'a'::TIME(123) WITHOUT TIME ZONE`, 32565, ``},
{`SELECT 'a'::TIMETZ(123)`, 26097, `type with precision`},
{`SELECT 'a'::TIME(123) WITH TIME ZONE`, 32565, ``},
{`SELECT TIME(3) 'a'`, 32565, ``},
{`SELECT TIMETZ(3) 'a'`, 26097, `type with precision`},

{`SELECT a(b) 'c'`, 0, `a(...) SCONST`},
{`SELECT (a,b) OVERLAPS (c,d)`, 0, `overlaps`},
{`SELECT UNIQUE (SELECT b)`, 0, `UNIQUE predicate`},
Expand Down
28 changes: 22 additions & 6 deletions pkg/sql/parser/sql.y
Original file line number Diff line number Diff line change
Expand Up @@ -7298,19 +7298,35 @@ const_datetime:
}
| TIME opt_timezone
{
if $2.bool() { return unimplementedWithIssueDetail(sqllex, 26097, "type") }
$$.val = types.Time
if $2.bool() {
$$.val = types.TimeTZ
} else {
$$.val = types.Time
}
}
| TIME '(' iconst32 ')' opt_timezone
{
prec := $3.int32()
if prec != 6 {
return unimplementedWithIssue(sqllex, 32565)
if prec < 0 || prec > 6 {
sqllex.Error(fmt.Sprintf("precision %d out of range", prec))
return 1
}
if $5.bool() {
$$.val = types.MakeTimeTZ(prec)
} else {
$$.val = types.MakeTime(prec)
}
$$.val = types.MakeTime(prec)
}
| TIMETZ { $$.val = types.TimeTZ }
| TIMETZ '(' ICONST ')' { return unimplementedWithIssueDetail(sqllex, 26097, "type with precision") }
| TIMETZ '(' iconst32 ')'
{
prec := $3.int32()
if prec < 0 || prec > 6 {
sqllex.Error(fmt.Sprintf("precision %d out of range", prec))
return 1
}
$$.val = types.MakeTimeTZ(prec)
}
| TIMESTAMP opt_timezone
{
if $2.bool() {
Expand Down
4 changes: 2 additions & 2 deletions pkg/sql/pgwire/pgwirebase/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,13 @@ func DecodeOidDatum(
}
return d, nil
case oid.T_time:
d, err := tree.ParseDTime(nil, string(b))
d, err := tree.ParseDTime(nil, string(b), time.Microsecond)
if err != nil {
return nil, pgerror.Newf(pgcode.Syntax, "could not parse string %q as time", b)
}
return d, nil
case oid.T_timetz:
d, err := tree.ParseDTimeTZ(ctx, string(b))
d, err := tree.ParseDTimeTZ(ctx, string(b), time.Microsecond)
if err != nil {
return nil, pgerror.Newf(pgcode.Syntax, "could not parse string %q as timetz", b)
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/sql/schemachange/alter_column_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ var classifiers = map[types.Family]map[types.Family]classifier{
types.TimestampFamily: classifierPrecision,
types.TimestampTZFamily: classifierPrecision,
},
types.TimeFamily: {
types.TimeFamily: classifierPrecision,
},
types.TimeTZFamily: {
types.TimeTZFamily: classifierPrecision,
},
}

// classifierHardestOf creates a composite classifier that returns the
Expand Down
18 changes: 16 additions & 2 deletions pkg/sql/schemachange/alter_column_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ func TestColumnConversions(t *testing.T) {
"STRING(5)": {
"BYTES": ColumnConversionTrivial,
},

"TIME": {
"TIME": ColumnConversionTrivial,
"TIME(5)": ColumnConversionValidate,
},
"TIMETZ": {
"TIMETZ": ColumnConversionTrivial,
"TIMETZ(5)": ColumnConversionValidate,
},
"TIMESTAMP": {
"TIMESTAMPTZ": ColumnConversionTrivial,
"TIMESTAMP": ColumnConversionTrivial,
Expand Down Expand Up @@ -224,9 +233,11 @@ func TestColumnConversions(t *testing.T) {

case types.TimeFamily,
types.TimestampFamily,
types.TimestampTZFamily:
types.TimestampTZFamily,
types.TimeTZFamily:

const timeOnly = "15:04:05"
const timeOnlyWithZone = "15:04:05 -0700"
const noZone = "2006-01-02 15:04:05"
const withZone = "2006-01-02 15:04:05 -0700"

Expand All @@ -238,6 +249,8 @@ func TestColumnConversions(t *testing.T) {
fromFmt = noZone
case types.TimestampTZFamily:
fromFmt = withZone
case types.TimeTZFamily:
fromFmt = timeOnlyWithZone
}

// Always use a non-UTC zone for this test
Expand All @@ -255,7 +268,8 @@ func TestColumnConversions(t *testing.T) {
case
types.TimeFamily,
types.TimestampFamily,
types.TimestampTZFamily:
types.TimestampTZFamily,
types.TimeTZFamily:
// We're going to re-parse the text as though we're in UTC
// so that we can drop the TZ info.
if parsed, err := time.ParseInLocation(fromFmt, now, time.UTC); err == nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/sql/sem/builtins/builtins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ func TestExtractStringFromTimeTZ(t *testing.T) {

for _, tc := range testCases {
t.Run(fmt.Sprintf("%s_%s", tc.timeSpan, tc.timeTZString), func(t *testing.T) {
timeTZ, err := tree.ParseDTimeTZ(nil, tc.timeTZString)
timeTZ, err := tree.ParseDTimeTZ(nil, tc.timeTZString, time.Microsecond)
assert.NoError(t, err)

datum, err := extractStringFromTimeTZ(timeTZ, tc.timeSpan)
Expand Down
10 changes: 9 additions & 1 deletion pkg/sql/sem/tree/constant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,14 @@ func mustParseDDate(t *testing.T, s string) tree.Datum {
return d
}
func mustParseDTime(t *testing.T, s string) tree.Datum {
d, err := tree.ParseDTime(nil, s)
d, err := tree.ParseDTime(nil, s, time.Microsecond)
if err != nil {
t.Fatal(err)
}
return d
}
func mustParseDTimeTZ(t *testing.T, s string) tree.Datum {
d, err := tree.ParseDTimeTZ(nil, s, time.Microsecond)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -254,6 +261,7 @@ var parseFuncs = map[*types.T]func(*testing.T, string) tree.Datum{
types.Bool: mustParseDBool,
types.Date: mustParseDDate,
types.Time: mustParseDTime,
types.TimeTZ: mustParseDTimeTZ,
types.Timestamp: mustParseDTimestamp,
types.TimestampTZ: mustParseDTimestampTZ,
types.Interval: mustParseDInterval,
Expand Down
Loading

0 comments on commit f769516

Please sign in to comment.