use super::ty::{IntervalFields, TimeZone};
use crate::dialect::StringLiteralSyntax;
use crate::vocab::{Meta, Span};
use std::borrow::Cow;
use std::fmt;
use std::iter::Peekable;
use std::str::Chars;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
pub struct Literal {
pub kind: LiteralKind,
pub meta: Meta,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
pub enum LiteralKind {
Integer,
Float,
Decimal,
String,
Boolean(bool),
Null,
Date,
Time {
time_zone: TimeZone,
},
Timestamp {
time_zone: TimeZone,
},
Interval {
fields: Option<IntervalFields>,
precision: Option<u32>,
},
BitString {
radix: BitStringRadix,
},
Money,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
pub enum BitStringRadix {
Binary,
Hex,
}
impl BitStringRadix {
const fn is_digit(self, byte: u8) -> bool {
match self {
Self::Binary => matches!(byte, b'0' | b'1'),
Self::Hex => byte.is_ascii_hexdigit(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
pub enum LiteralExpected {
Integer,
DecimalText,
String,
Boolean,
Null,
Temporal,
BitString,
Money,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LiteralValueErrorKind {
WrongKind {
expected: LiteralExpected,
actual: LiteralKind,
},
MissingSource,
InvalidSourceRange,
InvalidInteger,
InvalidString,
InvalidBitString,
InvalidMoney,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LiteralValueError {
kind: LiteralValueErrorKind,
span: Span,
}
impl LiteralValueError {
pub const fn new(kind: LiteralValueErrorKind, span: Span) -> Self {
Self { kind, span }
}
pub const fn kind(&self) -> &LiteralValueErrorKind {
&self.kind
}
pub const fn span(&self) -> Span {
self.span
}
}
impl fmt::Display for LiteralValueError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
LiteralValueErrorKind::WrongKind { expected, actual } => {
write!(
f,
"literal kind mismatch: expected {expected}, found {actual}"
)
}
LiteralValueErrorKind::MissingSource => {
f.write_str("literal has no backing source text")
}
LiteralValueErrorKind::InvalidSourceRange => {
f.write_str("literal span does not slice the supplied source")
}
LiteralValueErrorKind::InvalidInteger => {
f.write_str("literal source text is not a valid i64")
}
LiteralValueErrorKind::InvalidString => {
f.write_str("literal source text is not a supported string literal")
}
LiteralValueErrorKind::InvalidBitString => {
f.write_str("literal source text is not a valid bit-string constant")
}
LiteralValueErrorKind::InvalidMoney => {
f.write_str("literal source text is not a valid money literal")
}
}
}
}
impl std::error::Error for LiteralValueError {}
impl fmt::Display for LiteralExpected {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Integer => "integer",
Self::DecimalText => "decimal text",
Self::String => "string",
Self::Boolean => "boolean",
Self::Null => "null",
Self::Temporal => "temporal",
Self::BitString => "bit string",
Self::Money => "money",
})
}
}
impl fmt::Display for LiteralKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Integer => f.write_str("integer"),
Self::Float => f.write_str("float"),
Self::Decimal => f.write_str("decimal"),
Self::String => f.write_str("string"),
Self::Boolean(true) => f.write_str("true"),
Self::Boolean(false) => f.write_str("false"),
Self::Null => f.write_str("null"),
Self::Date => f.write_str("date"),
Self::Time { .. } => f.write_str("time"),
Self::Timestamp { .. } => f.write_str("timestamp"),
Self::Interval { .. } => f.write_str("interval"),
Self::BitString { radix } => match radix {
BitStringRadix::Binary => f.write_str("bit string (binary)"),
BitStringRadix::Hex => f.write_str("bit string (hex)"),
},
Self::Money => f.write_str("money"),
}
}
}
impl Literal {
pub fn source_text<'source>(
&self,
source: &'source str,
) -> Result<&'source str, LiteralValueError> {
if self.meta.span.is_synthetic() {
return Err(self.error(LiteralValueErrorKind::MissingSource));
}
source
.get(self.meta.span.start() as usize..self.meta.span.end() as usize)
.ok_or_else(|| self.error(LiteralValueErrorKind::InvalidSourceRange))
}
pub fn as_i64(&self, source: &str) -> Result<i64, LiteralValueError> {
self.expect_kind(LiteralExpected::Integer, |kind| {
matches!(kind, LiteralKind::Integer)
})?;
materialize_i64(self.source_text(source)?)
.map_err(|()| self.error(LiteralValueErrorKind::InvalidInteger))
}
pub fn as_decimal_text<'source>(
&self,
source: &'source str,
) -> Result<Cow<'source, str>, LiteralValueError> {
self.expect_kind(LiteralExpected::DecimalText, |kind| {
matches!(
kind,
LiteralKind::Integer | LiteralKind::Float | LiteralKind::Decimal
)
})?;
materialize_decimal_text(self.source_text(source)?)
.map_err(|()| self.error(LiteralValueErrorKind::InvalidInteger))
}
pub fn as_str<'source>(
&self,
source: &'source str,
) -> Result<Cow<'source, str>, LiteralValueError> {
self.as_str_in(source, StringLiteralSyntax::ANSI)
}
pub fn as_str_in<'source>(
&self,
source: &'source str,
syntax: StringLiteralSyntax,
) -> Result<Cow<'source, str>, LiteralValueError> {
self.expect_kind(LiteralExpected::String, |kind| {
matches!(kind, LiteralKind::String)
})?;
let text = self.source_text(source)?;
materialize_concatenated_string(text, syntax.backslash_escapes)
.map_err(|()| self.error(LiteralValueErrorKind::InvalidString))
}
pub fn charset_introducer<'source>(
&self,
source: &'source str,
) -> Result<Option<&'source str>, LiteralValueError> {
self.expect_kind(LiteralExpected::String, |kind| {
matches!(kind, LiteralKind::String)
})?;
let text = self.source_text(source)?;
Ok(charset_introducer_quote_offset(text).map(|open| &text[1..open]))
}
pub fn as_temporal_text<'source>(
&self,
source: &'source str,
) -> Result<Cow<'source, str>, LiteralValueError> {
self.expect_kind(LiteralExpected::Temporal, |kind| {
matches!(
kind,
LiteralKind::Date
| LiteralKind::Time { .. }
| LiteralKind::Timestamp { .. }
| LiteralKind::Interval { .. }
)
})?;
let token = temporal_string_token(self.source_text(source)?)
.ok_or_else(|| self.error(LiteralValueErrorKind::InvalidString))?;
materialize_concatenated_string(token, false)
.map_err(|()| self.error(LiteralValueErrorKind::InvalidString))
}
pub fn as_bit_text<'source>(
&self,
source: &'source str,
) -> Result<Cow<'source, str>, LiteralValueError> {
let radix = match self.kind {
LiteralKind::BitString { radix } => radix,
_ => return Err(self.wrong_kind(LiteralExpected::BitString)),
};
let body = concatenated_bit_body(self.source_text(source)?)
.ok_or_else(|| self.error(LiteralValueErrorKind::InvalidBitString))?;
if !body.bytes().all(|byte| radix.is_digit(byte)) {
return Err(self.error(LiteralValueErrorKind::InvalidBitString));
}
Ok(body)
}
pub fn as_money_text<'source>(
&self,
source: &'source str,
) -> Result<Cow<'source, str>, LiteralValueError> {
self.expect_kind(LiteralExpected::Money, |kind| {
matches!(kind, LiteralKind::Money)
})?;
let body = money_body(self.source_text(source)?)
.ok_or_else(|| self.error(LiteralValueErrorKind::InvalidMoney))?;
Ok(Cow::Borrowed(body))
}
pub fn as_bool(&self) -> Result<bool, LiteralValueError> {
match self.kind {
LiteralKind::Boolean(value) => Ok(value),
_ => Err(self.wrong_kind(LiteralExpected::Boolean)),
}
}
pub fn as_null(&self) -> Result<(), LiteralValueError> {
self.expect_kind(LiteralExpected::Null, |kind| {
matches!(kind, LiteralKind::Null)
})
}
pub fn is_null(&self) -> bool {
matches!(self.kind, LiteralKind::Null)
}
fn expect_kind(
&self,
expected: LiteralExpected,
matches: impl FnOnce(&LiteralKind) -> bool,
) -> Result<(), LiteralValueError> {
if matches(&self.kind) {
Ok(())
} else {
Err(self.wrong_kind(expected))
}
}
fn wrong_kind(&self, expected: LiteralExpected) -> LiteralValueError {
self.error(LiteralValueErrorKind::WrongKind {
expected,
actual: self.kind.clone(),
})
}
fn error(&self, kind: LiteralValueErrorKind) -> LiteralValueError {
LiteralValueError::new(kind, self.meta.span)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StringLiteralBody<'source> {
Standard(&'source str),
PostgresEscape(&'source str),
DollarQuoted(&'source str),
Unicode {
body: &'source str,
escape: char,
},
}
fn materialize_concatenated_string(text: &str, backslash: bool) -> Result<Cow<'_, str>, ()> {
let Some(first_end) = first_segment_end(text, backslash) else {
return materialize_string(text, backslash);
};
let tail = text[first_end..].trim_start();
if !(tail.starts_with('\'') || tail.starts_with('"')) {
return materialize_string(text, backslash);
}
let mut out = String::new();
let mut rest = text;
loop {
let end = first_segment_end(rest, backslash).ok_or(())?;
out.push_str(&materialize_string(&rest[..end], backslash)?);
rest = rest[end..].trim_start();
if rest.is_empty() {
break;
}
}
Ok(Cow::Owned(out))
}
fn first_segment_end(text: &str, dialect_backslash: bool) -> Option<usize> {
let bytes = text.as_bytes();
let (open, prefix_backslash) = match bytes {
[b'E' | b'e', b'\'', ..] => (1, true),
[b'N' | b'n', b'\'', ..] => (1, false),
[b'U' | b'u', b'&', b'\'', ..] => (2, false),
[b'B' | b'b' | b'X' | b'x', b'\'', ..] => (1, false),
[b'\'', ..] | [b'"', ..] => (0, false),
[b'_', ..] => (charset_introducer_quote_offset(text)?, false),
_ => return None,
};
let quote = bytes[open]; let backslash = prefix_backslash || dialect_backslash;
let mut i = open + 1; while i < bytes.len() {
match bytes[i] {
b'\\' if backslash => i += 2,
b if b == quote => {
if bytes.get(i + 1) == Some("e) {
i += 2; } else {
return Some(i + 1); }
}
_ => i += 1,
}
}
None
}
fn materialize_string(text: &str, backslash: bool) -> Result<Cow<'_, str>, ()> {
if string_literal_embeds_nul(text) {
return Err(());
}
match string_literal_body(text).ok_or(())? {
StringLiteralBody::Standard(body) => {
let quote = text.as_bytes().last().copied().unwrap_or(b'\'');
materialize_standard_string(body, quote, backslash)
}
StringLiteralBody::PostgresEscape(body) => materialize_postgres_escape_string(body),
StringLiteralBody::DollarQuoted(body) => Ok(Cow::Borrowed(body)),
StringLiteralBody::Unicode { body, escape } => materialize_unicode_string(body, escape),
}
}
fn string_literal_body(text: &str) -> Option<StringLiteralBody<'_>> {
if let Some(body) = text
.strip_prefix('\'')
.and_then(|text| text.strip_suffix('\''))
{
return Some(StringLiteralBody::Standard(body));
}
if let Some(body) = text
.strip_prefix('"')
.and_then(|text| text.strip_suffix('"'))
{
return Some(StringLiteralBody::Standard(body));
}
if let Some(body) = postgres_escape_body(text) {
return Some(StringLiteralBody::PostgresEscape(body));
}
if matches!(text.as_bytes(), [b'N' | b'n', b'\'', ..])
&& text.len() >= 3
&& text.ends_with('\'')
{
return Some(StringLiteralBody::Standard(&text[2..text.len() - 1]));
}
if let Some(open) = charset_introducer_quote_offset(text) {
let rest = &text[open..];
let body = rest
.strip_prefix('\'')
.and_then(|body| body.strip_suffix('\''))
.or_else(|| {
rest.strip_prefix('"')
.and_then(|body| body.strip_suffix('"'))
});
if let Some(body) = body {
return Some(StringLiteralBody::Standard(body));
}
}
if let Some(unicode) = unicode_string_body(text) {
return Some(unicode);
}
dollar_quoted_body(text).map(StringLiteralBody::DollarQuoted)
}
fn unicode_string_body(text: &str) -> Option<StringLiteralBody<'_>> {
let (body, escape) = unicode_lexeme_body(text, b'\'')?;
Some(StringLiteralBody::Unicode { body, escape })
}
fn unicode_lexeme_body(text: &str, quote: u8) -> Option<(&str, char)> {
let rest = text
.strip_prefix("U&")
.or_else(|| text.strip_prefix("u&"))?;
let bytes = rest.as_bytes();
if bytes.first() != Some("e) {
return None;
}
let close = terminating_quote(bytes, quote)?;
let body = &rest[1..close];
let tail = rest.get(close + 1..)?.trim_start();
let escape = if tail.is_empty() {
'\\'
} else {
uescape_char(tail)?
};
Some((body, escape))
}
fn terminating_quote(bytes: &[u8], quote: u8) -> Option<usize> {
let mut index = 1;
while index < bytes.len() {
if bytes[index] == quote {
if bytes.get(index + 1) == Some("e) {
index += 2;
continue;
}
return Some(index);
}
index += 1;
}
None
}
fn uescape_char(tail: &str) -> Option<char> {
tail.get(..7)
.filter(|kw| kw.eq_ignore_ascii_case("UESCAPE"))?;
let arg = tail[7..].trim_start();
parse_uescape_argument(arg)
}
fn parse_uescape_argument(arg: &str) -> Option<char> {
let inner = arg.strip_prefix('\'')?.strip_suffix('\'')?;
let value = materialize_standard_string(inner, b'\'', false).ok()?;
let mut chars = value.chars();
let escape = chars.next()?;
if chars.next().is_some() || !is_legal_uescape_char(escape) {
return None;
}
Some(escape)
}
pub fn uescape_argument_is_legal(arg: &str) -> bool {
parse_uescape_argument(arg).is_some()
}
fn is_legal_uescape_char(ch: char) -> bool {
!(ch.is_ascii_hexdigit() || ch == '+' || ch == '\'' || ch == '"' || ch.is_whitespace())
}
fn bit_string_body(text: &str) -> Option<&str> {
let rest = match text.as_bytes() {
[b'B' | b'b' | b'X' | b'x', b'\'', ..] => &text[1..],
_ => return None,
};
rest.strip_prefix('\'')?.strip_suffix('\'')
}
fn concatenated_bit_body(text: &str) -> Option<Cow<'_, str>> {
let first_end = first_segment_end(text, false)?;
if first_end == text.len() {
return bit_string_body(text).map(Cow::Borrowed);
}
let mut out = String::from(bit_string_body(&text[..first_end])?);
let mut rest = text[first_end..].trim_start();
while !rest.is_empty() {
let end = first_segment_end(rest, false)?;
out.push_str(plain_quoted_inner(&rest[..end])?);
rest = rest[end..].trim_start();
}
Some(Cow::Owned(out))
}
fn plain_quoted_inner(segment: &str) -> Option<&str> {
segment.strip_prefix('\'')?.strip_suffix('\'')
}
fn money_body(text: &str) -> Option<&str> {
text.strip_prefix('$')
}
fn split_sign(text: &str) -> (&str, &str) {
match text.as_bytes().first() {
Some(b'+' | b'-') => text.split_at(1),
_ => ("", text),
}
}
pub fn split_radix_prefix(unsigned: &str) -> (u32, &str) {
match unsigned.as_bytes() {
[b'0', b'x' | b'X', ..] => (16, &unsigned[2..]),
[b'0', b'o' | b'O', ..] => (8, &unsigned[2..]),
[b'0', b'b' | b'B', ..] => (2, &unsigned[2..]),
_ => (10, unsigned),
}
}
fn is_radix_digit(byte: u8, base: u32) -> bool {
match base {
16 => byte.is_ascii_hexdigit(),
8 => matches!(byte, b'0'..=b'7'),
2 => matches!(byte, b'0' | b'1'),
_ => byte.is_ascii_digit(),
}
}
fn strip_digit_separators(body: &str, base: u32) -> Result<Cow<'_, str>, ()> {
let bytes = body.as_bytes();
if !bytes.contains(&b'_') {
return Ok(Cow::Borrowed(body));
}
for (index, &byte) in bytes.iter().enumerate() {
if byte != b'_' {
continue;
}
let after_digit = index
.checked_sub(1)
.is_some_and(|prev| is_radix_digit(bytes[prev], base));
let before_digit = bytes
.get(index + 1)
.is_some_and(|&next| is_radix_digit(next, base));
if !(after_digit && before_digit) {
return Err(());
}
}
Ok(Cow::Owned(body.chars().filter(|&ch| ch != '_').collect()))
}
fn materialize_i64(text: &str) -> Result<i64, ()> {
let (sign, unsigned) = split_sign(text);
let (base, body) = split_radix_prefix(unsigned);
let digits = strip_digit_separators(body, base)?;
if base == 10 {
return match digits {
Cow::Borrowed(_) => text.parse::<i64>(),
Cow::Owned(stripped) => format!("{sign}{stripped}").parse::<i64>(),
}
.map_err(|_| ());
}
let value = i64::from_str_radix(&digits, base).map_err(|_| ())?;
if sign == "-" {
value.checked_neg().ok_or(())
} else {
Ok(value)
}
}
fn materialize_decimal_text(text: &str) -> Result<Cow<'_, str>, ()> {
let (sign, unsigned) = split_sign(text);
let (base, body) = split_radix_prefix(unsigned);
let digits = strip_digit_separators(body, base)?;
if base == 10 {
return Ok(match digits {
Cow::Borrowed(_) => Cow::Borrowed(text),
Cow::Owned(stripped) => Cow::Owned(format!("{sign}{stripped}")),
});
}
let magnitude = i128::from_str_radix(&digits, base).map_err(|_| ())?;
let value = if sign == "-" { -magnitude } else { magnitude };
Ok(Cow::Owned(value.to_string()))
}
fn dollar_open_delim_len(bytes: &[u8]) -> Option<usize> {
if bytes.first() != Some(&b'$') {
return None;
}
let mut index = 1;
match bytes.get(index).copied()? {
b'$' => {}
byte if is_dollar_tag_start(byte) => {
index += 1;
while bytes
.get(index)
.is_some_and(|&byte| is_dollar_tag_continue(byte))
{
index += 1;
}
if bytes.get(index) != Some(&b'$') {
return None;
}
}
_ => return None,
}
Some(index + 1)
}
fn dollar_quoted_body(text: &str) -> Option<&str> {
let bytes = text.as_bytes();
let delim_len = dollar_open_delim_len(bytes)?;
if text.len() < delim_len * 2 {
return None;
}
let close_start = text.len() - delim_len;
if bytes[..delim_len] != bytes[close_start..] {
return None;
}
Some(&text[delim_len..close_start])
}
fn is_dollar_tag_start(byte: u8) -> bool {
byte == b'_' || byte.is_ascii_alphabetic() || byte >= 0x80
}
fn is_dollar_tag_continue(byte: u8) -> bool {
is_dollar_tag_start(byte) || byte.is_ascii_digit()
}
fn charset_introducer_quote_offset(text: &str) -> Option<usize> {
let name_len = text
.strip_prefix('_')?
.bytes()
.take_while(|&byte| is_charset_name_byte(byte))
.count();
if name_len == 0 {
return None;
}
let open = 1 + name_len; matches!(text.as_bytes().get(open), Some(b'\'' | b'"')).then_some(open)
}
fn is_charset_name_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
fn temporal_string_token(text: &str) -> Option<&str> {
let bytes = text.as_bytes();
let first_quote = bytes.iter().position(|&byte| byte == b'\'');
let first_dollar = bytes.iter().position(|&byte| byte == b'$');
let dollar_first = match (first_quote, first_dollar) {
(Some(quote), Some(dollar)) => dollar < quote,
(None, Some(_)) => true,
_ => false,
};
if dollar_first {
let start = first_dollar?;
let len = dollar_quoted_token_len(&bytes[start..])?;
return text.get(start..start + len);
}
let quote = first_quote?;
let start = if quote >= 1
&& matches!(bytes[quote - 1], b'E' | b'e')
&& (quote < 2 || !is_dollar_tag_continue(bytes[quote - 2]))
{
quote - 1
} else {
quote
};
let end = bytes.iter().rposition(|&byte| byte == b'\'')?;
text.get(start..=end)
}
fn dollar_quoted_token_len(bytes: &[u8]) -> Option<usize> {
let delim_len = dollar_open_delim_len(bytes)?;
let delim = &bytes[..delim_len];
let mut close = delim_len;
while close + delim_len <= bytes.len() {
if &bytes[close..close + delim_len] == delim {
return Some(close + delim_len);
}
close += 1;
}
None
}
fn materialize_standard_string(body: &str, quote: u8, backslash: bool) -> Result<Cow<'_, str>, ()> {
let bytes = body.as_bytes();
let has_backslash = backslash && bytes.contains(&b'\\');
if !bytes.contains("e) && !has_backslash {
return Ok(Cow::Borrowed(body));
}
let mut out = String::with_capacity(body.len());
let mut segment_start = 0;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' if backslash => {
out.push_str(&body[segment_start..i]);
i += 1;
consume_mysql_escape(body, &mut i, &mut out);
segment_start = i;
}
b if b == quote => {
if bytes.get(i + 1) != Some("e) {
return Err(());
}
out.push_str(&body[segment_start..i]);
out.push(quote as char); i += 2;
segment_start = i;
}
_ => i += 1,
}
}
out.push_str(&body[segment_start..]);
Ok(Cow::Owned(out))
}
fn consume_mysql_escape(body: &str, index: &mut usize, out: &mut String) {
let Some(byte) = body.as_bytes().get(*index).copied() else {
out.push('\\'); return;
};
match byte {
b'0' => out.push('\0'),
b'\'' => out.push('\''),
b'"' => out.push('"'),
b'b' => out.push('\u{08}'),
b'n' => out.push('\n'),
b'r' => out.push('\r'),
b't' => out.push('\t'),
b'Z' => out.push('\u{1a}'),
b'\\' => out.push('\\'),
b'%' => out.push_str("\\%"),
b'_' => out.push_str("\\_"),
_ => {
let ch = body[*index..]
.chars()
.next()
.expect("index is within the body");
out.push(ch);
*index += ch.len_utf8();
return;
}
}
*index += 1;
}
fn materialize_postgres_escape_string(body: &str) -> Result<Cow<'_, str>, ()> {
let bytes = body.as_bytes();
if !bytes.contains(&b'\\') && !bytes.contains(&b'\'') {
return Ok(Cow::Borrowed(body));
}
let mut out = Vec::with_capacity(body.len());
scan_postgres_escape_string(body, &mut out)?;
String::from_utf8(out).map(Cow::Owned).map_err(|_| ())
}
pub fn postgres_escape_string_is_valid(text: &str) -> bool {
let Some(body) = postgres_escape_body(text) else {
return true;
};
let bytes = body.as_bytes();
if !bytes.contains(&b'\\') && !bytes.contains(&b'\'') {
return true;
}
let mut tail = UTF8_TAIL_START;
scan_postgres_escape_string(body, &mut tail).is_ok() && tail == UTF8_TAIL_START
}
pub fn string_literal_embeds_nul(text: &str) -> bool {
text.as_bytes().contains(&0)
}
fn scan_postgres_escape_string<S: EscapeSink>(body: &str, out: &mut S) -> Result<(), ()> {
let bytes = body.as_bytes();
let mut segment_start = 0;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\'' => {
if bytes.get(i + 1) != Some(&b'\'') {
return Err(());
}
out.utf8_run(&bytes[segment_start..i])?;
push_escape_byte(out, b'\'')?;
i += 2;
segment_start = i;
}
b'\\' => {
out.utf8_run(&bytes[segment_start..i])?;
i += 1;
consume_postgres_escape(body, &mut i, out)?;
segment_start = i;
}
_ => i += 1,
}
}
out.utf8_run(&bytes[segment_start..])
}
trait EscapeSink {
fn utf8_run(&mut self, bytes: &[u8]) -> Result<(), ()>;
fn raw_byte(&mut self, byte: u8) -> Result<(), ()>;
}
impl EscapeSink for Vec<u8> {
fn utf8_run(&mut self, bytes: &[u8]) -> Result<(), ()> {
self.extend_from_slice(bytes);
Ok(())
}
fn raw_byte(&mut self, byte: u8) -> Result<(), ()> {
self.push(byte);
Ok(())
}
}
type Utf8Tail = (u8, u8, u8);
const UTF8_TAIL_START: Utf8Tail = (0, 0x80, 0xBF);
fn utf8_tail_push(tail: &mut Utf8Tail, byte: u8) -> Result<(), ()> {
let (remaining, lo, hi) = *tail;
if remaining > 0 {
if !(lo..=hi).contains(&byte) {
return Err(());
}
*tail = (remaining - 1, 0x80, 0xBF);
return Ok(());
}
*tail = match byte {
0x00..=0x7F => (0, 0x80, 0xBF), 0xC2..=0xDF => (1, 0x80, 0xBF), 0xE0 => (2, 0xA0, 0xBF), 0xED => (2, 0x80, 0x9F), 0xE1..=0xEC | 0xEE..=0xEF => (2, 0x80, 0xBF), 0xF0 => (3, 0x90, 0xBF), 0xF4 => (3, 0x80, 0x8F), 0xF1..=0xF3 => (3, 0x80, 0xBF), _ => return Err(()),
};
Ok(())
}
impl EscapeSink for Utf8Tail {
fn utf8_run(&mut self, bytes: &[u8]) -> Result<(), ()> {
bytes
.iter()
.try_for_each(|&byte| utf8_tail_push(self, byte))
}
fn raw_byte(&mut self, byte: u8) -> Result<(), ()> {
utf8_tail_push(self, byte)
}
}
fn postgres_escape_body(text: &str) -> Option<&str> {
(matches!(text.as_bytes(), [b'E' | b'e', b'\'', ..]) && text.len() >= 3 && text.ends_with('\''))
.then(|| &text[2..text.len() - 1])
}
fn consume_postgres_escape<S: EscapeSink>(
body: &str,
index: &mut usize,
out: &mut S,
) -> Result<(), ()> {
let bytes = body.as_bytes();
let Some(byte) = bytes.get(*index).copied() else {
return Err(());
};
match byte {
b'b' => {
push_escape_byte(out, 0x08)?;
*index += 1;
}
b'f' => {
push_escape_byte(out, 0x0c)?;
*index += 1;
}
b'n' => {
push_escape_byte(out, b'\n')?;
*index += 1;
}
b'r' => {
push_escape_byte(out, b'\r')?;
*index += 1;
}
b't' => {
push_escape_byte(out, b'\t')?;
*index += 1;
}
b'0'..=b'7' => consume_octal_escape(bytes, index, out)?,
b'x' => consume_hex_escape(body, index, out)?,
b'u' => consume_unicode_escape(bytes, index, out, 4)?,
b'U' => consume_unicode_escape(bytes, index, out, 8)?,
_ => consume_escaped_char(body, index, out)?,
}
Ok(())
}
fn consume_octal_escape<S: EscapeSink>(
bytes: &[u8],
index: &mut usize,
out: &mut S,
) -> Result<(), ()> {
let mut value = 0_u32;
let mut count = 0;
while count < 3 {
let Some(byte @ b'0'..=b'7') = bytes.get(*index).copied() else {
break;
};
value = value * 8 + u32::from(byte - b'0');
*index += 1;
count += 1;
}
push_escape_byte(out, value as u8)
}
fn consume_hex_escape<S: EscapeSink>(body: &str, index: &mut usize, out: &mut S) -> Result<(), ()> {
let bytes = body.as_bytes();
let start = *index + 1;
let Some(first) = bytes.get(start).and_then(|byte| hex_value(*byte)) else {
return consume_escaped_char(body, index, out);
};
let mut value = first;
*index = start + 1;
if let Some(second) = bytes.get(*index).and_then(|byte| hex_value(*byte)) {
value = value * 16 + second;
*index += 1;
}
push_escape_byte(out, value as u8)
}
fn consume_unicode_escape<S: EscapeSink>(
bytes: &[u8],
index: &mut usize,
out: &mut S,
width: usize,
) -> Result<(), ()> {
let mut value = 0_u32;
let start = *index + 1;
let end = start.checked_add(width).ok_or(())?;
for byte in bytes.get(start..end).ok_or(())? {
let digit = hex_value(*byte).ok_or(())?;
value = value * 16 + digit;
}
let ch = char::from_u32(value).ok_or(())?;
if ch == '\0' {
return Err(());
}
let mut buf = [0; 4];
out.utf8_run(ch.encode_utf8(&mut buf).as_bytes())?;
*index = end;
Ok(())
}
fn consume_escaped_char<S: EscapeSink>(
body: &str,
index: &mut usize,
out: &mut S,
) -> Result<(), ()> {
let ch = body[*index..].chars().next().ok_or(())?;
if ch == '\0' {
return Err(());
}
let mut buf = [0; 4];
out.utf8_run(ch.encode_utf8(&mut buf).as_bytes())?;
*index += ch.len_utf8();
Ok(())
}
fn push_escape_byte<S: EscapeSink>(out: &mut S, byte: u8) -> Result<(), ()> {
if byte == 0 {
return Err(());
}
out.raw_byte(byte)
}
fn hex_value(byte: u8) -> Option<u32> {
match byte {
b'0'..=b'9' => Some(u32::from(byte - b'0')),
b'a'..=b'f' => Some(u32::from(byte - b'a' + 10)),
b'A'..=b'F' => Some(u32::from(byte - b'A' + 10)),
_ => None,
}
}
fn materialize_unicode_string(body: &str, escape: char) -> Result<Cow<'_, str>, ()> {
if !body.contains(escape) && !body.contains('\'') {
return Ok(Cow::Borrowed(body));
}
let mut out = String::with_capacity(body.len());
scan_unicode_escape_string(body, escape, '\'', &mut out)?;
Ok(Cow::Owned(out))
}
pub fn materialize_unicode_ident(text: &str) -> Option<Cow<'_, str>> {
let (body, escape) = unicode_lexeme_body(text, b'"')?;
if !body.contains(escape) && !body.contains('"') {
return Some(Cow::Borrowed(body));
}
let mut out = String::with_capacity(body.len());
scan_unicode_escape_string(body, escape, '"', &mut out).ok()?;
Some(Cow::Owned(out))
}
pub fn is_unicode_ident(text: &str) -> bool {
matches!(text.as_bytes(), [b'U' | b'u', b'&', b'"', ..])
}
pub fn unicode_escape_string_is_valid(text: &str) -> bool {
let Some(StringLiteralBody::Unicode { body, escape }) = unicode_string_body(text) else {
return true;
};
if !body.contains(escape) && !body.contains('\'') {
return true;
}
scan_unicode_escape_string(body, escape, '\'', &mut ()).is_ok()
}
fn scan_unicode_escape_string<S: UnicodeEscapeSink>(
body: &str,
escape: char,
quote: char,
out: &mut S,
) -> Result<(), ()> {
let mut chars = body.chars().peekable();
while let Some(ch) = chars.next() {
if ch == quote {
if chars.next() != Some(quote) {
return Err(());
}
out.emit(quote)?;
} else if ch == escape {
match chars.peek().copied() {
Some(next) if next == escape => {
chars.next();
out.emit(escape)?;
}
Some('+') => {
chars.next();
let code_point = read_unicode_hex(&mut chars, 6)?;
push_code_point(out, code_point)?;
}
Some(_) => {
let code_point = read_unicode_hex(&mut chars, 4)?;
if is_high_surrogate(code_point) {
if chars.next() != Some(escape) {
return Err(());
}
let low = read_unicode_hex(&mut chars, 4)?;
if !is_low_surrogate(low) {
return Err(());
}
let combined = 0x1_0000 + ((code_point - 0xD800) << 10) + (low - 0xDC00);
push_code_point(out, combined)?;
} else {
push_code_point(out, code_point)?;
}
}
None => return Err(()),
}
} else {
out.emit(ch)?;
}
}
Ok(())
}
trait UnicodeEscapeSink {
fn emit(&mut self, ch: char) -> Result<(), ()>;
}
impl UnicodeEscapeSink for String {
fn emit(&mut self, ch: char) -> Result<(), ()> {
self.push(ch);
Ok(())
}
}
impl UnicodeEscapeSink for () {
fn emit(&mut self, _ch: char) -> Result<(), ()> {
Ok(())
}
}
fn read_unicode_hex(chars: &mut Peekable<Chars<'_>>, width: usize) -> Result<u32, ()> {
let mut value = 0_u32;
for _ in 0..width {
let digit = chars.next().and_then(|ch| ch.to_digit(16)).ok_or(())?;
value = value * 16 + digit;
}
Ok(value)
}
fn push_code_point<S: UnicodeEscapeSink>(out: &mut S, code_point: u32) -> Result<(), ()> {
if code_point == 0 {
return Err(());
}
out.emit(char::from_u32(code_point).ok_or(())?)
}
fn is_high_surrogate(code_point: u32) -> bool {
(0xD800..=0xDBFF).contains(&code_point)
}
fn is_low_surrogate(code_point: u32) -> bool {
(0xDC00..=0xDFFF).contains(&code_point)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{RenderConfig, RenderCtx, RenderExt};
use crate::vocab::{NodeId, Resolver, Symbol};
#[test]
fn integer_accessor_materializes_i64() {
let literal = literal(LiteralKind::Integer, 7, 10);
assert_eq!(literal.as_i64("SELECT 123"), Ok(123));
}
#[test]
fn integer_accessor_reports_invalid_text() {
let literal = literal(LiteralKind::Integer, 0, 19);
assert_eq!(
literal
.as_i64("9223372036854775808")
.expect_err("overflows i64")
.kind(),
&LiteralValueErrorKind::InvalidInteger,
);
}
#[test]
fn decimal_text_accessor_borrows_exact_numeric_spelling() {
let literal = literal(LiteralKind::Float, 7, 12);
let text = literal
.as_decimal_text("SELECT 1.5e3")
.expect("float text materializes");
assert!(matches!(text, Cow::Borrowed("1.5e3")));
}
#[test]
fn decimal_kind_materializes_as_decimal_text_like_float() {
let literal = literal(LiteralKind::Decimal, 7, 12);
let text = literal
.as_decimal_text("SELECT 1.5e3")
.expect("decimal text materializes");
assert!(matches!(text, Cow::Borrowed("1.5e3")));
assert_eq!(LiteralKind::Decimal.to_string(), "decimal");
}
#[test]
fn string_accessor_borrows_unescaped_body() {
let literal = literal(LiteralKind::String, 7, 13);
let text = literal
.as_str("SELECT 'cafe'")
.expect("string text materializes");
assert!(matches!(text, Cow::Borrowed("cafe")));
}
#[test]
fn string_accessor_allocates_only_for_doubled_quote_unescape() {
let literal = literal(LiteralKind::String, 7, 14);
let text = literal
.as_str("SELECT 'it''s'")
.expect("escaped string materializes");
assert!(matches!(text, Cow::Owned(_)));
assert_eq!(text, "it's");
}
#[test]
fn string_accessor_strips_mysql_charset_introducer() {
let src = "SELECT _utf8mb4'cafe'";
let literal = literal(LiteralKind::String, 7, src.len() as u32);
let text = literal.as_str(src).expect("charset string materializes");
assert!(matches!(text, Cow::Borrowed("cafe")));
}
#[test]
fn charset_introducer_accessor_recovers_the_name_from_the_span() {
let src = "SELECT _latin1'x'";
let introduced = literal(LiteralKind::String, 7, src.len() as u32);
assert_eq!(
introduced.charset_introducer(src).expect("string literal"),
Some("latin1"),
);
let plain = literal(LiteralKind::String, 7, 10);
assert_eq!(
plain.charset_introducer("SELECT 'x'").expect("string"),
None
);
let integer = literal(LiteralKind::Integer, 7, 8);
assert!(integer.charset_introducer("SELECT 1").is_err());
}
#[test]
fn charset_introducer_accessor_recovers_the_name_from_a_double_quoted_span() {
let src = r#"SELECT _latin1"x""#;
let introduced = literal(LiteralKind::String, 7, src.len() as u32);
assert_eq!(
introduced.charset_introducer(src).expect("string literal"),
Some("latin1"),
);
let plain_dq = literal(LiteralKind::String, 7, 10); assert_eq!(
plain_dq
.as_str(r#"SELECT "x""#)
.expect("double-quoted value"),
"x",
);
assert_eq!(
introduced.as_str(src).expect("charset double-quoted value"),
"x",
);
}
#[test]
fn string_accessor_materializes_double_quoted_values() {
let src = r#"SELECT "abc""#;
let plain = literal(LiteralKind::String, 7, src.len() as u32);
let value = plain.as_str(src).expect("double-quoted value materializes");
assert!(matches!(value, Cow::Borrowed("abc")));
let src = r#"SELECT "a""b""#;
let doubled = literal(LiteralKind::String, 7, src.len() as u32);
let value = doubled.as_str(src).expect("doubled quote un-doubles");
assert!(matches!(value, Cow::Owned(_)));
assert_eq!(value, r#"a"b"#);
}
#[test]
fn string_accessor_applies_mysql_backslash_escapes_in_both_quote_forms() {
for src in [r"'a\nb'", r#""a\nb""#] {
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(
literal
.as_str_in(src, StringLiteralSyntax::MYSQL)
.expect("escape materializes"),
"a\nb",
"escape for {src:?}",
);
assert_eq!(
literal.as_str(src).expect("literal backslash"),
r"a\nb",
"ansi for {src:?}",
);
}
}
#[test]
fn string_accessor_mysql_pattern_and_unknown_escapes() {
for (src, value) in [
(r"'a\%b'", r"a\%b"),
(r"'a\_b'", r"a\_b"),
(r"'a\qb'", "aqb"),
(r"'a\\b'", r"a\b"),
(r"'a\'b'", "a'b"),
(r"'it''s'", "it's"),
] {
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(
literal
.as_str_in(src, StringLiteralSyntax::MYSQL)
.expect("materializes"),
value,
"for {src:?}",
);
}
}
#[test]
fn string_accessor_mysql_control_escapes_decode_to_bytes() {
let src = r"'\0\b\t\r\Z'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(
literal
.as_str_in(src, StringLiteralSyntax::MYSQL)
.expect("control escapes"),
"\0\u{08}\t\r\u{1a}",
);
}
#[test]
fn single_quote_doubling_path_is_unchanged_by_escape_support() {
let borrow = literal(LiteralKind::String, 0, "'plain'".len() as u32);
assert!(matches!(
borrow.as_str("'plain'").expect("borrow"),
Cow::Borrowed("plain"),
));
assert!(matches!(
borrow
.as_str_in("'plain'", StringLiteralSyntax::MYSQL)
.expect("borrow"),
Cow::Borrowed("plain"),
));
let doubled = literal(LiteralKind::String, 0, "'it''s'".len() as u32);
assert_eq!(doubled.as_str("'it''s'").expect("unescape"), "it's");
}
#[test]
fn double_quoted_and_escaped_strings_render_exact_source() {
for src in [r#""a""b""#, r"'a\nb'"] {
let literal = literal(LiteralKind::String, 0, src.len() as u32);
let _ = literal
.as_str_in(src, StringLiteralSyntax::MYSQL)
.expect("materializes");
assert_eq!(rendered(&literal, src), *src, "round-trip for {src:?}");
}
}
#[test]
fn mysql_multi_segment_and_multiline_double_quoted_strings_materialize() {
let concat = literal(LiteralKind::String, 0, "'a\\'b'\n'c'".len() as u32);
assert_eq!(
concat
.as_str_in("'a\\'b'\n'c'", StringLiteralSyntax::MYSQL)
.expect("concatenation materializes"),
"a'bc",
);
let multiline = literal(LiteralKind::String, 0, "\"a\nb\"".len() as u32);
assert_eq!(
multiline
.as_str_in("\"a\nb\"", StringLiteralSyntax::MYSQL)
.expect("multiline double-quoted"),
"a\nb",
);
}
#[test]
fn string_accessor_borrows_plain_postgres_escape_string_body() {
let literal = literal(LiteralKind::String, 7, 15);
let text = literal
.as_str("SELECT E'plain'")
.expect("escape string materializes");
assert!(matches!(text, Cow::Borrowed("plain")));
}
#[test]
fn string_accessor_materializes_postgres_c_style_escapes() {
let src = "SELECT E'line\\nquote\\''";
let literal = literal(LiteralKind::String, 7, src.len() as u32);
let text = literal.as_str(src).expect("escape string materializes");
assert!(matches!(text, Cow::Owned(_)));
assert_eq!(text, "line\nquote'");
}
#[test]
fn string_accessor_materializes_postgres_numeric_and_unicode_escapes() {
let src = "SELECT e'\\141\\x62\\u0063\\U00000064'";
let literal = literal(LiteralKind::String, 7, src.len() as u32);
let text = literal.as_str(src).expect("escape string materializes");
assert_eq!(text, "abcd");
}
#[test]
fn string_accessor_validates_postgres_escape_output_as_utf8() {
let src = "SELECT E'\\xc3\\xa9'";
let valid = literal(LiteralKind::String, 7, src.len() as u32);
let text = valid.as_str(src).expect("UTF-8 byte escapes materialize");
assert_eq!(text, "é");
let src = "SELECT E'\\xff'";
let invalid = literal(LiteralKind::String, 7, src.len() as u32);
let error = invalid.as_str(src).expect_err("invalid UTF-8 byte escape");
assert_eq!(error.kind(), &LiteralValueErrorKind::InvalidString);
}
#[test]
fn string_accessor_rejects_invalid_postgres_escape_values() {
for src in ["SELECT E'\\u12'", "SELECT E'\\0'", "SELECT E'\\U00110000'"] {
let literal = literal(LiteralKind::String, 7, src.len() as u32);
let error = literal.as_str(src).expect_err("invalid escape value");
assert_eq!(error.kind(), &LiteralValueErrorKind::InvalidString);
}
}
#[test]
fn string_accessor_treats_unknown_postgres_escapes_as_literal_characters() {
let src = "SELECT E'\\q\\x'";
let literal = literal(LiteralKind::String, 7, src.len() as u32);
let text = literal.as_str(src).expect("unknown escapes materialize");
assert_eq!(text, "qx");
}
const POSTGRES_ESCAPE_REJECTS: &[&str] = &[
"E'\\u12'", "E'\\u'", "E'\\u006'", "E'\\U0000006'", "E'\\uD800'", "E'\\U00110000'", "E'\\0'", "E'\\x00'", "E'\\U00000000'", "E'\\377'", "E'\\xff'", "E'\\xc3'", "E'\\xc3a'", ];
const POSTGRES_ESCAPE_ACCEPTS: &[&str] = &[
"E''", "E'plain'", "E'\\q'", "E'\\x'", "E'\\xg'", "E'\\q\\x'", "E'\\b\\f\\n\\r\\t'", "E'\\xc3\\xa9'", "E'\\141\\x62c\\U00000064'", "E'it''s'", ];
#[test]
fn parse_time_check_rejects_what_postgres_rejects() {
for text in POSTGRES_ESCAPE_REJECTS {
assert!(
!postgres_escape_string_is_valid(text),
"should reject {text:?}",
);
}
}
#[test]
fn parse_time_check_accepts_what_postgres_accepts() {
for text in POSTGRES_ESCAPE_ACCEPTS {
assert!(
postgres_escape_string_is_valid(text),
"should accept {text:?}",
);
}
}
#[test]
fn parse_time_check_skips_non_escape_string_literals() {
for text in ["'plain'", "$$a\\n$$", "U&'\\D800'", "N'x'"] {
assert!(postgres_escape_string_is_valid(text), "for {text:?}");
}
}
#[test]
fn parse_time_check_and_as_str_agree() {
for text in POSTGRES_ESCAPE_REJECTS
.iter()
.chain(POSTGRES_ESCAPE_ACCEPTS)
{
let literal = literal(LiteralKind::String, 0, text.len() as u32);
let as_str_ok = literal.as_str(text).is_ok();
let valid = postgres_escape_string_is_valid(text);
assert_eq!(
valid, as_str_ok,
"parse-time check ({valid}) and as_str ({as_str_ok}) disagree for {text:?}",
);
}
}
const NUL_STRING_REJECTS: &[&str] = &[
"'a\0b'", "'\0'", "E'a\0b'", "N'a\0b'", "$$a\0b$$", "$tag$a\0b$tag$", "U&'a\0b'", ];
const NUL_STRING_ACCEPTS: &[&str] = &["'ab'", "'a\x01b'", "E'ab'", "N'ab'", "$$ab$$", "U&'ab'"];
#[test]
fn parse_time_nul_check_detects_only_a_raw_nul() {
for text in NUL_STRING_REJECTS {
assert!(
string_literal_embeds_nul(text),
"should detect a NUL in {text:?}",
);
}
for text in NUL_STRING_ACCEPTS {
assert!(!string_literal_embeds_nul(text), "should accept {text:?}");
}
}
#[test]
fn parse_time_nul_check_and_as_str_agree() {
for text in NUL_STRING_REJECTS.iter().chain(NUL_STRING_ACCEPTS) {
let literal = literal(LiteralKind::String, 0, text.len() as u32);
let as_str_ok = literal.as_str(text).is_ok();
let embeds_nul = string_literal_embeds_nul(text);
assert_eq!(
embeds_nul, !as_str_ok,
"nul-check ({embeds_nul}) and as_str ok ({as_str_ok}) disagree for {text:?}",
);
}
}
#[test]
fn string_accessor_borrows_dollar_quoted_body_verbatim() {
let src = "SELECT $tag$a\\n'b$tag$";
let literal = literal(LiteralKind::String, 7, src.len() as u32);
let text = literal
.as_str(src)
.expect("dollar-quoted string materializes");
assert!(matches!(text, Cow::Borrowed("a\\n'b")));
}
#[test]
fn string_accessor_strips_national_string_prefix() {
let src = "N'caf''e'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(
literal.as_str(src).expect("national string materializes"),
"caf'e",
);
}
#[test]
fn string_accessor_applies_unicode_escapes_with_default_backslash() {
let src = r"U&'d\0061t\+000061'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(
literal.as_str(src).expect("unicode string materializes"),
"data",
);
}
#[test]
fn string_accessor_borrows_unicode_string_without_escapes() {
let src = "U&'plain'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
let text = literal.as_str(src).expect("unicode string materializes");
assert!(matches!(text, Cow::Borrowed("plain")));
}
#[test]
fn string_accessor_applies_unicode_uescape_override() {
let src = "U&'d!0061t!+000061' UESCAPE '!'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(
literal.as_str(src).expect("uescape override materializes"),
"data",
);
}
#[test]
fn unicode_ident_decodes_the_double_quoted_identifier_surface() {
for (src, value) in [
(r#"U&"d\0061ta""#, "data"),
(r#"U&"d0061ta""#, "d0061ta"), (r#"U&"real\00A7_name""#, "real\u{00A7}_name"),
(r#"U&"""""#, "\""), (r#"U&"a'b""#, "a'b"), (r#"U&"d!0061ta" UESCAPE '!'"#, "data"),
(r#"U&"\ZZZZ" UESCAPE '!'"#, "\\ZZZZ"), ] {
assert_eq!(
materialize_unicode_ident(src).expect("valid U&\"...\" identifier"),
value,
"decoded value for {src:?}",
);
assert!(is_unicode_ident(src), "prefix recognised for {src:?}");
}
}
#[test]
fn unicode_ident_rejects_the_escapes_postgres_rejects() {
for src in [
r#"U&"\ZZZZ""#, r#"U&"\d800""#, r#"U&"\0000""#, r#"U&"\+110000""#, ] {
assert!(
materialize_unicode_ident(src).is_none(),
"{src:?} must be rejected",
);
}
}
#[test]
fn string_accessor_combines_unicode_surrogate_pairs() {
let src = r"U&'\D800\DC00'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(
literal.as_str(src).expect("surrogate pair materializes"),
"\u{10000}",
);
}
#[test]
fn string_accessor_unfolds_doubled_escape_and_quote_in_unicode_string() {
let src = r"U&'a\\b''c'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(literal.as_str(src).expect("materializes"), "a\\b'c");
}
#[test]
fn string_accessor_rejects_malformed_unicode_escapes() {
for src in [
r"U&'\XYZW'", r"U&'\D800'", r"U&'\+110000'", r"U&'\0000'", r"U&'\'", "U&'ab' UESCAPE 'a'", "U&'ab' UESCAPE 'xy'", ] {
let literal = literal(LiteralKind::String, 0, src.len() as u32);
let error = literal.as_str(src).expect_err("malformed unicode escape");
assert_eq!(
error.kind(),
&LiteralValueErrorKind::InvalidString,
"for {src:?}",
);
}
}
#[test]
fn bit_string_accessor_borrows_validated_digits() {
let binary = literal(
LiteralKind::BitString {
radix: BitStringRadix::Binary,
},
0,
"B'1010'".len() as u32,
);
assert_eq!(binary.as_bit_text("B'1010'").expect("binary body"), "1010");
let hex = literal(
LiteralKind::BitString {
radix: BitStringRadix::Hex,
},
0,
"X'1FF'".len() as u32,
);
assert_eq!(hex.as_bit_text("X'1FF'").expect("hex body"), "1FF");
}
#[test]
fn bit_string_accessor_rejects_out_of_radix_digits() {
let hex = literal(
LiteralKind::BitString {
radix: BitStringRadix::Hex,
},
0,
"X'1FG'".len() as u32,
);
assert_eq!(
hex.as_bit_text("X'1FG'")
.expect_err("G is not a hex digit")
.kind(),
&LiteralValueErrorKind::InvalidBitString,
);
let binary = literal(
LiteralKind::BitString {
radix: BitStringRadix::Binary,
},
0,
"B'012'".len() as u32,
);
assert_eq!(
binary
.as_bit_text("B'012'")
.expect_err("2 is not a binary digit")
.kind(),
&LiteralValueErrorKind::InvalidBitString,
);
}
#[test]
fn bit_string_accessor_rejects_non_bit_literal() {
let literal = literal(LiteralKind::String, 0, 5);
assert_eq!(
literal
.as_bit_text("'abc'")
.expect_err("string is not a bit string")
.kind(),
&LiteralValueErrorKind::WrongKind {
expected: LiteralExpected::BitString,
actual: LiteralKind::String,
},
);
}
#[test]
fn money_accessor_borrows_numeric_body() {
for (src, body) in [("$1234.56", "1234.56"), ("$100", "100"), ("$.5", ".5")] {
let literal = literal(LiteralKind::Money, 0, src.len() as u32);
let text = literal.as_money_text(src).expect("money body materializes");
assert!(matches!(text, Cow::Borrowed(_)), "borrowed for {src:?}");
assert_eq!(text, body, "body for {src:?}");
}
}
#[test]
fn money_accessor_rejects_non_money_literal() {
let literal = literal(LiteralKind::Integer, 0, 3);
assert_eq!(
literal
.as_money_text("100")
.expect_err("integer is not a money literal")
.kind(),
&LiteralValueErrorKind::WrongKind {
expected: LiteralExpected::Money,
actual: LiteralKind::Integer,
},
);
}
#[test]
fn money_accessor_reports_missing_currency_sigil() {
let literal = literal(LiteralKind::Money, 0, 3);
assert_eq!(
literal
.as_money_text("100")
.expect_err("money text without `$`")
.kind(),
&LiteralValueErrorKind::InvalidMoney,
);
}
#[test]
fn temporal_accessor_materializes_value_string_for_each_family() {
let cases: &[(&str, LiteralKind, &str)] = &[
("DATE '1998-12-01'", LiteralKind::Date, "1998-12-01"),
(
"TIME WITH TIME ZONE '12:00:00+00'",
LiteralKind::Time {
time_zone: TimeZone::WithTimeZone,
},
"12:00:00+00",
),
(
"TIMESTAMP '2020-01-01 00:00:00'",
LiteralKind::Timestamp {
time_zone: TimeZone::Unspecified,
},
"2020-01-01 00:00:00",
),
(
"INTERVAL '90' DAY",
LiteralKind::Interval {
fields: Some(IntervalFields::Day),
precision: None,
},
"90",
),
(
"INTERVAL '1-2' YEAR TO MONTH",
LiteralKind::Interval {
fields: Some(IntervalFields::YearToMonth),
precision: None,
},
"1-2",
),
];
for (src, kind, expected) in cases {
let literal = literal(kind.clone(), 0, src.len() as u32);
assert_eq!(
literal
.as_temporal_text(src)
.expect("temporal value materializes"),
*expected,
"value string for {src:?}",
);
}
}
#[test]
fn temporal_accessor_applies_escaping_to_dollar_and_escape_strings() {
let dollar = literal(
LiteralKind::Interval {
fields: Some(IntervalFields::Day),
precision: None,
},
0,
"INTERVAL $$90$$ DAY".len() as u32,
);
assert_eq!(
dollar
.as_temporal_text("INTERVAL $$90$$ DAY")
.expect("dollar-quoted value materializes"),
"90",
);
let src = "TIMESTAMP WITH TIME ZONE E'2020-01-01\\t00:00:00'";
let escape = literal(
LiteralKind::Timestamp {
time_zone: TimeZone::WithTimeZone,
},
0,
src.len() as u32,
);
assert_eq!(
escape
.as_temporal_text(src)
.expect("escape value materializes"),
"2020-01-01\t00:00:00",
);
}
#[test]
fn temporal_accessor_rejects_non_temporal_literal() {
let literal = literal(LiteralKind::String, 0, 4);
assert_eq!(
literal
.as_temporal_text("'ab'")
.expect_err("string is not a temporal literal")
.kind(),
&LiteralValueErrorKind::WrongKind {
expected: LiteralExpected::Temporal,
actual: LiteralKind::String,
},
);
}
#[test]
fn boolean_and_null_accessors_do_not_need_source() {
assert_eq!(synthetic(LiteralKind::Boolean(true)).as_bool(), Ok(true));
assert_eq!(synthetic(LiteralKind::Null).as_null(), Ok(()));
assert!(synthetic(LiteralKind::Null).is_null());
}
#[test]
fn wrong_kind_errors_name_expected_and_actual_kinds() {
let literal = synthetic(LiteralKind::String);
let error = literal.as_bool().expect_err("string is not boolean");
assert_eq!(
error.kind(),
&LiteralValueErrorKind::WrongKind {
expected: LiteralExpected::Boolean,
actual: LiteralKind::String,
},
);
}
#[test]
fn detached_literal_reports_missing_source() {
let literal = synthetic(LiteralKind::Integer);
let error = literal
.as_i64("1")
.expect_err("synthetic span has no slice");
assert_eq!(error.kind(), &LiteralValueErrorKind::MissingSource);
assert_eq!(error.span(), Span::SYNTHETIC);
}
#[test]
fn invalid_span_reports_source_range_error() {
let literal = literal(LiteralKind::String, 0, 20);
let error = literal.as_str("'x'").expect_err("span is out of range");
assert_eq!(error.kind(), &LiteralValueErrorKind::InvalidSourceRange);
}
#[test]
fn malformed_string_reports_invalid_string() {
let literal = literal(LiteralKind::String, 0, 4);
let error = literal
.as_str("oops")
.expect_err("not a single-quoted string literal");
assert_eq!(error.kind(), &LiteralValueErrorKind::InvalidString);
}
#[test]
fn string_accessor_rejects_truncated_escape_and_national_prefixes() {
for src in ["E'", "e'", "N'", "n'"] {
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(
literal
.as_str(src)
.expect_err("truncated prefix is not a valid string")
.kind(),
&LiteralValueErrorKind::InvalidString,
"for {src:?}",
);
}
}
#[test]
fn string_accessor_concatenates_newline_separated_segments() {
let src = "'foo'\n'bar'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
let value = literal.as_str(src).expect("adjacent strings concatenate");
assert!(matches!(value, Cow::Owned(_)));
assert_eq!(value, "foobar");
}
#[test]
fn string_accessor_concatenates_three_segments_and_collapses_doubled_quotes() {
let src = "'a''b'\n'c'\n'd''e'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
assert_eq!(literal.as_str(src).expect("three segments"), "a'bcd'e");
}
#[test]
fn string_accessor_concatenates_mixed_escape_and_standard_segments() {
let src = "E'\\x4'\n'1'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
let value = literal.as_str(src).expect("mixed segments concatenate");
assert_eq!(value, "\u{4}1");
}
#[test]
fn string_accessor_single_segment_with_body_newline_is_not_concatenation() {
let src = "'line1\nline2'";
let literal = literal(LiteralKind::String, 0, src.len() as u32);
let value = literal.as_str(src).expect("multi-line string body");
assert!(matches!(value, Cow::Borrowed("line1\nline2")));
}
#[test]
fn bit_string_accessor_concatenates_newline_separated_segments() {
let src = "B'1010'\n'0101'";
let literal = literal(
LiteralKind::BitString {
radix: BitStringRadix::Binary,
},
0,
src.len() as u32,
);
let value = literal
.as_bit_text(src)
.expect("adjacent bit strings concatenate");
assert!(matches!(value, Cow::Owned(_)));
assert_eq!(value, "10100101");
}
#[test]
fn bit_string_accessor_concatenation_still_validates_radix() {
let src = "B'10'\n'21'";
let literal = literal(
LiteralKind::BitString {
radix: BitStringRadix::Binary,
},
0,
src.len() as u32,
);
let error = literal
.as_bit_text(src)
.expect_err("`2` is not a binary digit");
assert_eq!(error.kind(), &LiteralValueErrorKind::InvalidBitString);
}
#[test]
fn integer_accessor_materializes_radix_and_separator_forms() {
for (src, value) in [
("0x1F", 31),
("0b1010", 10),
("0o17", 15),
("1_500_000", 1_500_000),
] {
let literal = literal(LiteralKind::Integer, 0, src.len() as u32);
assert_eq!(literal.as_i64(src), Ok(value), "value for {src:?}");
}
}
#[test]
fn integer_accessor_rejects_misplaced_separators() {
for src in ["_1", "1_", "1__2", "0x_1F", "0x1F_"] {
let literal = literal(LiteralKind::Integer, 0, src.len() as u32);
assert_eq!(
literal.as_i64(src).expect_err("misplaced separator").kind(),
&LiteralValueErrorKind::InvalidInteger,
"rejecting {src:?}",
);
}
}
#[test]
fn decimal_text_accessor_strips_separators_and_normalizes_radix() {
let underscores = literal(LiteralKind::Integer, 0, "1_500_000".len() as u32);
assert_eq!(
underscores
.as_decimal_text("1_500_000")
.expect("separators strip"),
"1500000",
);
let hex = literal(LiteralKind::Integer, 0, "0xFF".len() as u32);
assert_eq!(hex.as_decimal_text("0xFF").expect("hex normalises"), "255");
}
#[test]
fn radix_and_separator_literals_render_their_exact_source_spelling() {
for src in ["0x1F", "0b1010", "0o17", "1_500_000"] {
let literal = literal(LiteralKind::Integer, 0, src.len() as u32);
let _ = literal.as_i64(src).expect("materialises");
assert_eq!(rendered(&literal, src), *src, "round-trip for {src:?}");
}
}
struct NoSymbols;
impl Resolver for NoSymbols {
fn try_resolve(&self, _sym: Symbol) -> Option<&str> {
None
}
}
fn rendered(literal: &Literal, source: &str) -> String {
let resolver = NoSymbols;
let config = RenderConfig::default();
let ctx = RenderCtx::new(&resolver, source, &config);
literal.displayed(&ctx).to_string()
}
fn literal(kind: LiteralKind, start: u32, end: u32) -> Literal {
Literal {
kind,
meta: Meta::new(
Span::new(start, end),
NodeId::new(1).expect("non-zero node id"),
),
}
}
fn synthetic(kind: LiteralKind) -> Literal {
Literal {
kind,
meta: Meta::new(Span::SYNTHETIC, NodeId::new(1).expect("non-zero node id")),
}
}
}