Skip to content

Commit

Permalink
Support mixed duration units
Browse files Browse the repository at this point in the history
It is now possible to use a mixed duration unit like `1h30m`. The
duration units can be in whatever order as long as they are connected to
each other.

There is a change to the scanner. A token such as `10x` will be scanned
as a duration literal, but will then fail to parse as an invalid
duration. This should not be a breaking change as there is no situation
where `10m10` was a valid order of tokens for the parser.

Fixes #3634.
  • Loading branch information
jsternberg committed May 11, 2016
1 parent 8353b0c commit b0bc3ef
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 45 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Features

- [#3541](https://github.com/influxdata/influxdb/issues/3451): Update SHOW FIELD KEYS to return the field type with the field key.
- [#3634](https://github.com/influxdata/influxdb/issues/3634): Support mixed duration units.

## v0.13.0 [unreleased]

Expand Down
2 changes: 1 addition & 1 deletion cmd/influxd/run/server_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func init() {
&Query{
name: "create database should error with bad name",
command: `CREATE DATABASE 0xdb0`,
exp: `{"error":"error parsing query: found 0, expected identifier at line 1, char 17"}`,
exp: `{"error":"error parsing query: found 0xdb0, expected identifier at line 1, char 17"}`,
},
&Query{
name: "create database with retention duration should error with bad retention duration",
Expand Down
2 changes: 1 addition & 1 deletion cmd/influxd/run/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func TestServer_UserCommands(t *testing.T) {
&Query{
name: "bad create user request",
command: `CREATE USER 0xBAD WITH PASSWORD pwd1337`,
exp: `{"error":"error parsing query: found 0, expected identifier at line 1, char 13"}`,
exp: `{"error":"error parsing query: found 0xBAD, expected identifier at line 1, char 13"}`,
},
&Query{
name: "bad create user request, no name",
Expand Down
1 change: 0 additions & 1 deletion influxql/ast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,6 @@ func TestTimeRange(t *testing.T) {

// number literal
{expr: `time < 10`, min: `0001-01-01T00:00:00Z`, max: `1970-01-01T00:00:00.000000009Z`},
{expr: `time < 10i`, min: `0001-01-01T00:00:00Z`, max: `1970-01-01T00:00:00.000000009Z`},

// Equality
{expr: `time = '2000-01-01 00:00:00'`, min: `2000-01-01T00:00:00Z`, max: `2000-01-01T00:00:00.000000001Z`},
Expand Down
87 changes: 56 additions & 31 deletions influxql/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2536,41 +2536,66 @@ func ParseDuration(s string) (time.Duration, error) {
// Split string into individual runes.
a := split(s)

// Extract the unit of measure.
// If the last two characters are "ms" then parse as milliseconds.
// Otherwise just use the last character as the unit of measure.
var num, uom string
if len(s) > 2 && s[len(s)-2:] == "ms" {
num, uom = string(a[:len(a)-2]), "ms"
} else {
num, uom = string(a[:len(a)-1]), string(a[len(a)-1:])
}
// Start with a zero duration.
var d time.Duration
i := 0

// Parse the numeric part.
n, err := strconv.ParseInt(num, 10, 64)
if err != nil {
return 0, ErrInvalidDuration
// Check for a negative.
isNegative := false
if a[i] == '-' {
isNegative = true
i++
}

// Multiply by the unit of measure.
switch uom {
case "u", "µ":
return time.Duration(n) * time.Microsecond, nil
case "ms":
return time.Duration(n) * time.Millisecond, nil
case "s":
return time.Duration(n) * time.Second, nil
case "m":
return time.Duration(n) * time.Minute, nil
case "h":
return time.Duration(n) * time.Hour, nil
case "d":
return time.Duration(n) * 24 * time.Hour, nil
case "w":
return time.Duration(n) * 7 * 24 * time.Hour, nil
default:
return 0, ErrInvalidDuration
// Parsing loop.
for i < len(a) {
// Find the number portion.
start := i
for ; i < len(a) && isDigit(a[i]); i++ {
// Scan for the digits.
}

// Check if we reached the end of the string prematurely.
if i >= len(a) || i == start {
return 0, ErrInvalidDuration
}

// Parse the numeric part.
n, err := strconv.ParseInt(string(a[start:i]), 10, 64)
if err != nil {
return 0, ErrInvalidDuration
}

// Extract the unit of measure.
// If the last two characters are "ms" then parse as milliseconds.
// Otherwise just use the last character as the unit of measure.
switch a[i] {
case 'u', 'µ':
d += time.Duration(n) * time.Microsecond
case 'm':
if i+1 < len(a) && a[i+1] == 's' {
d += time.Duration(n) * time.Millisecond
i += 2
continue
}
d += time.Duration(n) * time.Minute
case 's':
d += time.Duration(n) * time.Second
case 'h':
d += time.Duration(n) * time.Hour
case 'd':
d += time.Duration(n) * 24 * time.Hour
case 'w':
d += time.Duration(n) * 7 * 24 * time.Hour
default:
return 0, ErrInvalidDuration
}
i += 1
}
if isNegative {
d = -d
}
return d, nil
}

// FormatDuration formats a duration to a string.
Expand Down
4 changes: 4 additions & 0 deletions influxql/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,10 @@ func TestParseDuration(t *testing.T) {
{s: `2h`, d: 2 * time.Hour},
{s: `2d`, d: 2 * 24 * time.Hour},
{s: `2w`, d: 2 * 7 * 24 * time.Hour},
{s: `1h30m`, d: time.Hour + 30*time.Minute},
{s: `30ms3000u`, d: 30*time.Millisecond + 3000*time.Microsecond},
{s: `-5s`, d: -5 * time.Second},
{s: `-5m30s`, d: -5*time.Minute - 30*time.Second},

{s: ``, err: "invalid duration"},
{s: `3`, err: "invalid duration"},
Expand Down
31 changes: 21 additions & 10 deletions influxql/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,21 +253,32 @@ func (s *Scanner) scanNumber() (tok Token, pos Pos, lit string) {

// Read as a duration or integer if it doesn't have a fractional part.
if !isDecimal {
// If the next rune is a duration unit (u,µ,ms,s) then return a duration token
if ch0, _ := s.r.read(); ch0 == 'u' || ch0 == ' || ch0 == 's' || ch0 == 'h' || ch0 == 'd' || ch0 == 'w' {
// If the next rune is a letter then this is a duration token.
if ch0, _ := s.r.read(); isLetter(ch0) || ch0 == 'µ' {
_, _ = buf.WriteRune(ch0)
return DURATIONVAL, pos, buf.String()
} else if ch0 == 'm' {
_, _ = buf.WriteRune(ch0)
if ch1, _ := s.r.read(); ch1 == 's' {
for {
ch1, _ := s.r.read()
if !isLetter(ch1) && ch1 != 'µ' {
s.r.unread()
break
}
_, _ = buf.WriteRune(ch1)
} else {
s.r.unread()
}

// Continue reading digits and letters as part of this token.
for {
if ch0, _ := s.r.read(); isLetter(ch0) || ch0 == 'µ' || isDigit(ch0) {
_, _ = buf.WriteRune(ch0)
} else {
s.r.unread()
break
}
}
return DURATIONVAL, pos, buf.String()
} else {
s.r.unread()
return INTEGER, pos, buf.String()
}
s.r.unread()
return INTEGER, pos, buf.String()
}
return NUMBER, pos, buf.String()
}
Expand Down
2 changes: 1 addition & 1 deletion influxql/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestScanner_Scan(t *testing.T) {
{s: `10h`, tok: influxql.DURATIONVAL, lit: `10h`},
{s: `10d`, tok: influxql.DURATIONVAL, lit: `10d`},
{s: `10w`, tok: influxql.DURATIONVAL, lit: `10w`},
{s: `10x`, tok: influxql.INTEGER, lit: `10`}, // non-duration unit
{s: `10x`, tok: influxql.DURATIONVAL, lit: `10x`}, // non-duration unit, but scanned as a duration value

// Keywords
{s: `ALL`, tok: influxql.ALL},
Expand Down

0 comments on commit b0bc3ef

Please sign in to comment.