-
Notifications
You must be signed in to change notification settings - Fork 625
Support interval literals #103
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,14 @@ pub enum Value { | |
Time(String), | ||
/// Timestamp literals, which include both a date and time | ||
Timestamp(String), | ||
/// INTERVAL literals, e.g. INTERVAL '12:34.56' MINUTE TO SECOND (2) | ||
Interval { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SQL allows an optional sign here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, but I haven't found a database that actually supports that yet. I'm going to omit for now because we don't seem to have a SQLSign type handy. I've left a note in the parsing code, though. |
||
value: String, | ||
leading_field: SQLDateTimeField, | ||
leading_precision: Option<u64>, | ||
last_field: Option<SQLDateTimeField>, | ||
fractional_seconds_precision: Option<u64>, | ||
}, | ||
/// NULL value in insert statements, | ||
Null, | ||
} | ||
|
@@ -37,11 +45,74 @@ impl ToString for Value { | |
Value::Date(v) => format!("DATE '{}'", escape_single_quote_string(v)), | ||
Value::Time(v) => format!("TIME '{}'", escape_single_quote_string(v)), | ||
Value::Timestamp(v) => format!("TIMESTAMP '{}'", escape_single_quote_string(v)), | ||
Value::Interval { | ||
value, | ||
leading_field: SQLDateTimeField::Second, | ||
leading_precision: Some(leading_precision), | ||
last_field, | ||
fractional_seconds_precision: Some(fractional_seconds_precision), | ||
} => { | ||
// When the leading field is SECOND, the parser guarantees that | ||
// the last field is None. | ||
assert!(last_field.is_none()); | ||
format!( | ||
"INTERVAL '{}' SECOND ({}, {})", | ||
escape_single_quote_string(value), | ||
leading_precision, | ||
fractional_seconds_precision | ||
) | ||
} | ||
Value::Interval { | ||
value, | ||
leading_field, | ||
leading_precision, | ||
last_field, | ||
fractional_seconds_precision, | ||
} => { | ||
let mut s = format!( | ||
"INTERVAL '{}' {}", | ||
escape_single_quote_string(value), | ||
leading_field.to_string() | ||
); | ||
if let Some(leading_precision) = leading_precision { | ||
s += &format!(" ({})", leading_precision); | ||
} | ||
if let Some(last_field) = last_field { | ||
s += &format!(" TO {}", last_field.to_string()); | ||
} | ||
if let Some(fractional_seconds_precision) = fractional_seconds_precision { | ||
s += &format!(" ({})", fractional_seconds_precision); | ||
} | ||
s | ||
} | ||
Value::Null => "NULL".to_string(), | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq, Hash)] | ||
pub enum SQLDateTimeField { | ||
Year, | ||
Month, | ||
Day, | ||
Hour, | ||
Minute, | ||
Second, | ||
} | ||
|
||
impl ToString for SQLDateTimeField { | ||
fn to_string(&self) -> String { | ||
match self { | ||
SQLDateTimeField::Year => "YEAR".to_string(), | ||
SQLDateTimeField::Month => "MONTH".to_string(), | ||
SQLDateTimeField::Day => "DAY".to_string(), | ||
SQLDateTimeField::Hour => "HOUR".to_string(), | ||
SQLDateTimeField::Minute => "MINUTE".to_string(), | ||
SQLDateTimeField::Second => "SECOND".to_string(), | ||
} | ||
} | ||
} | ||
|
||
fn escape_single_quote_string(s: &str) -> String { | ||
let mut escaped = String::new(); | ||
for c in s.chars() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -195,6 +195,7 @@ impl Parser { | |
"DATE" => Ok(ASTNode::SQLValue(Value::Date(self.parse_literal_string()?))), | ||
"EXISTS" => self.parse_exists_expression(), | ||
"EXTRACT" => self.parse_extract_expression(), | ||
"INTERVAL" => self.parse_literal_interval(), | ||
"NOT" => Ok(ASTNode::SQLUnary { | ||
operator: SQLOperator::Not, | ||
expr: Box::new(self.parse_subexpr(Self::UNARY_NOT_PREC)?), | ||
|
@@ -425,20 +426,7 @@ impl Parser { | |
|
||
pub fn parse_extract_expression(&mut self) -> Result<ASTNode, ParserError> { | ||
self.expect_token(&Token::LParen)?; | ||
let tok = self.next_token(); | ||
let field = if let Some(Token::SQLWord(ref k)) = tok { | ||
match k.keyword.as_ref() { | ||
"YEAR" => SQLDateTimeField::Year, | ||
"MONTH" => SQLDateTimeField::Month, | ||
"DAY" => SQLDateTimeField::Day, | ||
"HOUR" => SQLDateTimeField::Hour, | ||
"MINUTE" => SQLDateTimeField::Minute, | ||
"SECOND" => SQLDateTimeField::Second, | ||
_ => self.expected("Date/time field inside of EXTRACT function", tok)?, | ||
} | ||
} else { | ||
self.expected("Date/time field inside of EXTRACT function", tok)? | ||
}; | ||
let field = self.parse_date_time_field()?; | ||
self.expect_keyword("FROM")?; | ||
let expr = self.parse_expr()?; | ||
self.expect_token(&Token::RParen)?; | ||
|
@@ -448,6 +436,90 @@ impl Parser { | |
}) | ||
} | ||
|
||
// This function parses date/time fields for both the EXTRACT function-like | ||
// operator and interval qualifiers. EXTRACT supports a wider set of | ||
// date/time fields than interval qualifiers, so this function may need to | ||
// be split in two. | ||
pub fn parse_date_time_field(&mut self) -> Result<SQLDateTimeField, ParserError> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that the list of keywords allowed in an INTERVAL qualifier is a subset of the list of keywords allowed in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You bet. I've added a comment. |
||
let tok = self.next_token(); | ||
if let Some(Token::SQLWord(ref k)) = tok { | ||
match k.keyword.as_ref() { | ||
"YEAR" => Ok(SQLDateTimeField::Year), | ||
"MONTH" => Ok(SQLDateTimeField::Month), | ||
"DAY" => Ok(SQLDateTimeField::Day), | ||
"HOUR" => Ok(SQLDateTimeField::Hour), | ||
"MINUTE" => Ok(SQLDateTimeField::Minute), | ||
"SECOND" => Ok(SQLDateTimeField::Second), | ||
_ => self.expected("date/time field", tok)?, | ||
} | ||
} else { | ||
self.expected("date/time field", tok)? | ||
} | ||
} | ||
|
||
/// Parse an INTERVAL literal. | ||
/// | ||
/// Some syntactically valid intervals: | ||
/// | ||
/// 1. `INTERVAL '1' DAY` | ||
/// 2. `INTERVAL '1-1' YEAR TO MONTH` | ||
/// 3. `INTERVAL '1' SECOND` | ||
/// 4. `INTERVAL '1:1:1.1' HOUR (5) TO SECOND (5)` | ||
/// 5. `INTERVAL '1.1' SECOND (2, 2)` | ||
/// 6. `INTERVAL '1:1' HOUR (5) TO MINUTE (5)` | ||
/// | ||
/// Note that we do not currently attempt to parse the quoted value. | ||
pub fn parse_literal_interval(&mut self) -> Result<ASTNode, ParserError> { | ||
// The SQL standard allows an optional sign before the value string, but | ||
// it is not clear if any implementations support that syntax, so we | ||
// don't currently try to parse it. (The sign can instead be included | ||
// inside the value string.) | ||
|
||
// The first token in an interval is a string literal which specifies | ||
// the duration of the interval. | ||
let value = self.parse_literal_string()?; | ||
|
||
// Following the string literal is a qualifier which indicates the units | ||
// of the duration specified in the string literal. | ||
// | ||
// Note that PostgreSQL allows omitting the qualifier, but we currently | ||
// require at least the leading field, in accordance with the ANSI spec. | ||
let leading_field = self.parse_date_time_field()?; | ||
|
||
let (leading_precision, last_field, fsec_precision) = | ||
if leading_field == SQLDateTimeField::Second { | ||
// SQL mandates special syntax for `SECOND TO SECOND` literals. | ||
// Instead of | ||
// `SECOND [(<leading precision>)] TO SECOND[(<fractional seconds precision>)]` | ||
// one must use the special format: | ||
// `SECOND [( <leading precision> [ , <fractional seconds precision>] )]` | ||
let last_field = None; | ||
let (leading_precision, fsec_precision) = self.parse_optional_precision_scale()?; | ||
(leading_precision, last_field, fsec_precision) | ||
} else { | ||
let leading_precision = self.parse_optional_precision()?; | ||
if self.parse_keyword("TO") { | ||
let last_field = Some(self.parse_date_time_field()?); | ||
let fsec_precision = if last_field == Some(SQLDateTimeField::Second) { | ||
self.parse_optional_precision()? | ||
} else { | ||
None | ||
}; | ||
(leading_precision, last_field, fsec_precision) | ||
} else { | ||
(leading_precision, None, None) | ||
} | ||
}; | ||
|
||
Ok(ASTNode::SQLValue(Value::Interval { | ||
value, | ||
leading_field, | ||
leading_precision, | ||
last_field, | ||
fractional_seconds_precision: fsec_precision, | ||
})) | ||
} | ||
|
||
/// Parse an operator following an expression | ||
pub fn parse_infix(&mut self, expr: ASTNode, precedence: u8) -> Result<ASTNode, ParserError> { | ||
debug!("parsing infix"); | ||
|
@@ -1182,6 +1254,10 @@ impl Parser { | |
} | ||
Ok(SQLType::Time) | ||
} | ||
// Interval types can be followed by a complicated interval | ||
// qualifier that we don't currently support. See | ||
// parse_interval_literal for a taste. | ||
"INTERVAL" => Ok(SQLType::Interval), | ||
"REGCLASS" => Ok(SQLType::Regclass), | ||
"TEXT" => { | ||
if self.consume_token(&Token::LBracket) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The INTERVAL type is supposed to have an
<interval qualifier>
, I think. Is it optional in some DBs? (edit:) Ah, Postgres supportsINTERVAL [<interval qualifier>] [(<seconds precision>)]
and appears limit the allowed<interval qualifier>
s to those without the "leading field precision".(It's fine with me if you don't want to implement this as part of this PR. I just noticed that we don't support seconds precision for TIME / TIMESTAMP either.)
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copying/pasting one of my comments from below that's relevant here:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine by me! Note though that
<interval qualifier>
after theINTERVAL
type is required by the standard, it's the way postgres allows to omit it is non-standard.