Extract the temporal expression that follows an AS-OF keyword at `offset` in `sql`. Returns `(resolved_ms, end_offset_exclusive)`. The expression is delimited by end-of-string or the next keyword-boundary token (a subsequent `AS OF`, or certain SQL keywords that cannot appear inside a temporal expression). Quoted string literals are consumed whole.
(sql: &str, offset: usize)
| 329 | /// token (a subsequent `AS OF`, or certain SQL keywords that cannot appear |
| 330 | /// inside a temporal expression). Quoted string literals are consumed whole. |
| 331 | fn parse_as_of_expr(sql: &str, offset: usize) -> Result<(i64, usize), TemporalParseError> { |
| 332 | let rest = &sql[offset..]; |
| 333 | let trimmed = rest.trim_start(); |
| 334 | let leading = rest.len() - trimmed.len(); |
| 335 | let abs_start = offset + leading; |
| 336 | |
| 337 | // If the next non-whitespace character opens a quoted string, consume |
| 338 | // through the closing quote. |
| 339 | if trimmed.starts_with('\'') || trimmed.starts_with('"') { |
| 340 | let quote = trimmed |
| 341 | .chars() |
| 342 | .next() |
| 343 | .expect("invariant: trimmed.starts_with('\\'' | '\"') guarantees at least one char"); |
| 344 | let inner_start = 1; |
| 345 | let close = trimmed[inner_start..].find(quote).ok_or_else(|| { |
| 346 | TemporalParseError(format!( |
| 347 | "AS OF temporal expression: unterminated string literal at offset {abs_start}" |
| 348 | )) |
| 349 | })?; |
| 350 | let end_rel = inner_start + close + 1; // include closing quote |
| 351 | let token = &trimmed[..end_rel]; |
| 352 | let ms = parse_temporal_expr(token)?; |
| 353 | return Ok((ms, abs_start + end_rel)); |
| 354 | } |
| 355 | |
| 356 | // Otherwise scan to the next whitespace-delimited boundary. We stop at: |
| 357 | // a) another `AS OF` sequence (another clause), |
| 358 | // b) certain SQL delimiter tokens: WHERE, LIMIT, ORDER, GROUP, HAVING, |
| 359 | // UNION, EXCEPT, INTERSECT, FETCH, FOR, OFFSET, semicolon. |
| 360 | let stop_tokens = [ |
| 361 | "AS OF", |
| 362 | "WHERE", |
| 363 | "LIMIT", |
| 364 | "ORDER", |
| 365 | "GROUP", |
| 366 | "HAVING", |
| 367 | "UNION", |
| 368 | "EXCEPT", |
| 369 | "INTERSECT", |
| 370 | "FETCH", |
| 371 | "OFFSET", |
| 372 | ";", |
| 373 | ]; |
| 374 | let upper_trimmed = trimmed.to_uppercase(); |
| 375 | let mut end_rel = trimmed.len(); |
| 376 | for stop in &stop_tokens { |
| 377 | // Only match at a word boundary (preceded by whitespace or start). |
| 378 | let mut search_from = 0; |
| 379 | while let Some(pos) = upper_trimmed[search_from..].find(stop) { |
| 380 | let abs_pos = search_from + pos; |
| 381 | let at_boundary = abs_pos == 0 |
| 382 | || upper_trimmed[..abs_pos].ends_with(|c: char| c.is_ascii_whitespace()); |
| 383 | if at_boundary { |
| 384 | end_rel = end_rel.min(abs_pos); |
| 385 | break; |
| 386 | } |
| 387 | search_from = abs_pos + 1; |
| 388 | } |
no test coverage detected