Skip to content

Commit 070d5b3

Browse files
committed
Explicit QuoteDelimitedString type
1 parent 69f642e commit 070d5b3

File tree

5 files changed

+89
-52
lines changed

5 files changed

+89
-52
lines changed

src/ast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ pub use self::trigger::{
110110

111111
pub use self::value::{
112112
escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString,
113-
NormalizationForm, TrimWhereField, Value, ValueWithSpan,
113+
NormalizationForm, QuoteDelimitedString, TrimWhereField, Value, ValueWithSpan,
114114
};
115115

116116
use crate::ast::helpers::key_value_options::KeyValueOptions;

src/ast/value.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use alloc::string::String;
2020

2121
use core::fmt;
22+
use std::fmt::Write;
2223

2324
#[cfg(feature = "bigdecimal")]
2425
use bigdecimal::BigDecimal;
@@ -168,11 +169,11 @@ pub enum Value {
168169
/// N'string value'
169170
NationalStringLiteral(String),
170171
/// Quote delimited literal. Examples `Q'{ab'c}'`, `Q'|ab'c|'`, `Q'|ab|c|'`
171-
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Literals.html)
172-
QuoteDelimitedStringLiteral(char, String, char),
172+
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Literals.html#GUID-1824CBAA-6E16-4921-B2A6-112FB02248DA)
173+
QuoteDelimitedStringLiteral(QuoteDelimitedString),
173174
/// "National" quote delimited literal. Examples `Q'{ab'c}'`, `Q'|ab'c|'`, `Q'|ab|c|'`
174-
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Literals.html)
175-
NationalQuoteDelimitedStringLiteral(char, String, char),
175+
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Literals.html#GUID-1824CBAA-6E16-4921-B2A6-112FB02248DA)
176+
NationalQuoteDelimitedStringLiteral(QuoteDelimitedString),
176177
/// X'hex value'
177178
HexStringLiteral(String),
178179

@@ -211,10 +212,10 @@ impl Value {
211212
| Value::EscapedStringLiteral(s)
212213
| Value::UnicodeStringLiteral(s)
213214
| Value::NationalStringLiteral(s)
214-
| Value::QuoteDelimitedStringLiteral(_, s, _)
215-
| Value::NationalQuoteDelimitedStringLiteral(_, s, _)
216215
| Value::HexStringLiteral(s) => Some(s),
217216
Value::DollarQuotedString(s) => Some(s.value),
217+
Value::QuoteDelimitedStringLiteral(s) => Some(s.value),
218+
Value::NationalQuoteDelimitedStringLiteral(s) => Some(s.value),
218219
_ => None,
219220
}
220221
}
@@ -250,8 +251,8 @@ impl fmt::Display for Value {
250251
Value::EscapedStringLiteral(v) => write!(f, "E'{}'", escape_escaped_string(v)),
251252
Value::UnicodeStringLiteral(v) => write!(f, "U&'{}'", escape_unicode_string(v)),
252253
Value::NationalStringLiteral(v) => write!(f, "N'{v}'"),
253-
Value::QuoteDelimitedStringLiteral(q1, s, q2) => write!(f, "Q'{q1}{s}{q2}'"),
254-
Value::NationalQuoteDelimitedStringLiteral(q1, s, q2) => write!(f, "NQ'{q1}{s}{q2}'"),
254+
Value::QuoteDelimitedStringLiteral(v) => v.fmt(f),
255+
Value::NationalQuoteDelimitedStringLiteral(v) => write!(f, "N{v}"),
255256
Value::HexStringLiteral(v) => write!(f, "X'{v}'"),
256257
Value::Boolean(v) => write!(f, "{v}"),
257258
Value::SingleQuotedByteStringLiteral(v) => write!(f, "B'{v}'"),
@@ -289,6 +290,32 @@ impl fmt::Display for DollarQuotedString {
289290
}
290291
}
291292

293+
/// A quote delimited string literal, e.g. `Q'_abc_'`.
294+
///
295+
/// See [Token::QuoteDelimitedStringLiteral] and/or
296+
/// [Token::NationalQuoteDelimitedStringLiteral].
297+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
298+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
299+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
300+
pub struct QuoteDelimitedString {
301+
/// the quote start character; i.e. the character _after_ the opening `Q'`
302+
pub start_quote: char,
303+
/// the string literal value itself
304+
pub value: String,
305+
/// the quote end character; i.e. the character _before_ the closing `'`
306+
pub end_quote: char,
307+
}
308+
309+
impl fmt::Display for QuoteDelimitedString {
310+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311+
f.write_str("Q'")?;
312+
f.write_char(self.start_quote)?;
313+
f.write_str(&self.value)?;
314+
f.write_char(self.end_quote)?;
315+
f.write_char('\'')
316+
}
317+
}
318+
292319
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
293320
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
294321
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/parser/mod.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,8 +1713,8 @@ impl<'a> Parser<'a> {
17131713
| Token::TripleSingleQuotedRawStringLiteral(_)
17141714
| Token::TripleDoubleQuotedRawStringLiteral(_)
17151715
| Token::NationalStringLiteral(_)
1716-
| Token::QuoteDelimitedStringLiteral(_, _, _)
1717-
| Token::NationalQuoteDelimitedStringLiteral(_, _, _)
1716+
| Token::QuoteDelimitedStringLiteral(_)
1717+
| Token::NationalQuoteDelimitedStringLiteral(_)
17181718
| Token::HexStringLiteral(_) => {
17191719
self.prev_token();
17201720
Ok(Expr::Value(self.parse_value()?))
@@ -2731,8 +2731,8 @@ impl<'a> Parser<'a> {
27312731
| Token::EscapedStringLiteral(_)
27322732
| Token::UnicodeStringLiteral(_)
27332733
| Token::NationalStringLiteral(_)
2734-
| Token::QuoteDelimitedStringLiteral(_, _, _)
2735-
| Token::NationalQuoteDelimitedStringLiteral(_, _, _)
2734+
| Token::QuoteDelimitedStringLiteral(_)
2735+
| Token::NationalQuoteDelimitedStringLiteral(_)
27362736
| Token::HexStringLiteral(_) => Some(Box::new(self.parse_expr()?)),
27372737
_ => self.expected(
27382738
"either filler, WITH, or WITHOUT in LISTAGG",
@@ -10660,11 +10660,11 @@ impl<'a> Parser<'a> {
1066010660
Token::NationalStringLiteral(ref s) => {
1066110661
ok_value(Value::NationalStringLiteral(s.to_string()))
1066210662
}
10663-
Token::QuoteDelimitedStringLiteral(q1, s, q2) => {
10664-
ok_value(Value::QuoteDelimitedStringLiteral(q1, s, q2))
10663+
Token::QuoteDelimitedStringLiteral(v) => {
10664+
ok_value(Value::QuoteDelimitedStringLiteral(v))
1066510665
}
10666-
Token::NationalQuoteDelimitedStringLiteral(q1, s, q2) => {
10667-
ok_value(Value::NationalQuoteDelimitedStringLiteral(q1, s, q2))
10666+
Token::NationalQuoteDelimitedStringLiteral(v) => {
10667+
ok_value(Value::NationalQuoteDelimitedStringLiteral(v))
1066810668
}
1066910669
Token::EscapedStringLiteral(ref s) => {
1067010670
ok_value(Value::EscapedStringLiteral(s.to_string()))

src/tokenizer.rs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ use crate::dialect::{
4646
};
4747
use crate::dialect::{Dialect, OracleDialect};
4848
use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX};
49-
use crate::{ast::DollarQuotedString, dialect::HiveDialect};
49+
use crate::{
50+
ast::{DollarQuotedString, QuoteDelimitedString},
51+
dialect::HiveDialect,
52+
};
5053

5154
/// SQL Token enumeration
5255
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -99,11 +102,11 @@ pub enum Token {
99102
/// "National" string literal: i.e: N'string'
100103
NationalStringLiteral(String),
101104
/// Quote delimited literal. Examples `Q'{ab'c}'`, `Q'|ab'c|'`, `Q'|ab|c|'`
102-
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Literals.html#GUID-1824CBAA-6E16-4921-B2A6-112FB02248DA)
103-
QuoteDelimitedStringLiteral(char, String, char),
105+
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Literals.html#GUID-1824CBAA-6E16-4921-B2A6-112FB02248DA)
106+
QuoteDelimitedStringLiteral(QuoteDelimitedString),
104107
/// "Nationa" quote delimited literal. Examples `NQ'{ab'c}'`, `NQ'|ab'c|'`, `NQ'|ab|c|'`
105-
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Literals.html)
106-
NationalQuoteDelimitedStringLiteral(char, String, char),
108+
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Literals.html#GUID-1824CBAA-6E16-4921-B2A6-112FB02248DA)
109+
NationalQuoteDelimitedStringLiteral(QuoteDelimitedString),
107110
/// "escaped" string literal, which are an extension to the SQL standard: i.e: e'first \n second' or E 'first \n second'
108111
EscapedStringLiteral(String),
109112
/// Unicode string literal: i.e: U&'first \000A second'
@@ -298,10 +301,8 @@ impl fmt::Display for Token {
298301
Token::TripleDoubleQuotedString(ref s) => write!(f, "\"\"\"{s}\"\"\""),
299302
Token::DollarQuotedString(ref s) => write!(f, "{s}"),
300303
Token::NationalStringLiteral(ref s) => write!(f, "N'{s}'"),
301-
Token::QuoteDelimitedStringLiteral(q1, ref s, q2) => write!(f, "Q'{q1}{s}{q2}'"),
302-
Token::NationalQuoteDelimitedStringLiteral(q1, ref s, q2) => {
303-
write!(f, "NQ'{q1}{s}{q2}'")
304-
}
304+
Token::QuoteDelimitedStringLiteral(ref s) => s.fmt(f),
305+
Token::NationalQuoteDelimitedStringLiteral(ref s) => write!(f, "N{s}"),
305306
Token::EscapedStringLiteral(ref s) => write!(f, "E'{s}'"),
306307
Token::UnicodeStringLiteral(ref s) => write!(f, "U&'{s}'"),
307308
Token::HexStringLiteral(ref s) => write!(f, "X'{s}'"),
@@ -2024,9 +2025,9 @@ impl<'a> Tokenizer<'a> {
20242025
}
20252026

20262027
/// Reads a quote delimited string without "backslash escaping" or a word
2027-
/// depending on whether `chars.next()` delivers a `'`.
2028+
/// depending on `chars.next()` delivering a `'`.
20282029
///
2029-
/// See <https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Literals.html>
2030+
/// See <https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Literals.html#GUID-1824CBAA-6E16-4921-B2A6-112FB02248DA>
20302031
fn tokenize_word_or_quote_delimited_string(
20312032
&self,
20322033
chars: &mut State,
@@ -2036,14 +2037,14 @@ impl<'a> Tokenizer<'a> {
20362037
// turns an identified quote string literal,
20372038
// ie. `(start-quote-char, string-literal, end-quote-char)`
20382039
// into a token
2039-
as_literal: fn(char, String, char) -> Token,
2040+
as_literal: fn(QuoteDelimitedString) -> Token,
20402041
) -> Result<Token, TokenizerError> {
20412042
match chars.peek() {
20422043
Some('\'') => {
20432044
chars.next();
20442045
// ~ determine the "quote character(s)"
20452046
let error_loc = chars.location();
2046-
let (start_quote_char, end_quote_char) = match chars.next() {
2047+
let (start_quote, end_quote) = match chars.next() {
20472048
// ~ "newline" is not allowed by Oracle's SQL Reference,
20482049
// but works with sql*plus nevertheless
20492050
None | Some(' ') | Some('\t') | Some('\r') | Some('\n') => {
@@ -2067,15 +2068,19 @@ impl<'a> Tokenizer<'a> {
20672068
),
20682069
};
20692070
// read the string literal until the "quote character" following a by literal quote
2070-
let mut s = String::new();
2071+
let mut value = String::new();
20712072
while let Some(ch) = chars.next() {
2072-
if ch == end_quote_char {
2073+
if ch == end_quote {
20732074
if let Some('\'') = chars.peek() {
20742075
chars.next(); // ~ consume the quote
2075-
return Ok(as_literal(start_quote_char, s, end_quote_char));
2076+
return Ok(as_literal(QuoteDelimitedString {
2077+
start_quote,
2078+
value,
2079+
end_quote,
2080+
}));
20762081
}
20772082
}
2078-
s.push(ch);
2083+
value.push(ch);
20792084
}
20802085
self.tokenizer_error(error_loc, "Unterminated string literal")
20812086
}

tests/sqlparser_oracle.rs

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
use pretty_assertions::assert_eq;
2222

2323
use sqlparser::{
24-
ast::{BinaryOperator, Expr, Ident, Value, ValueWithSpan},
24+
ast::{BinaryOperator, Expr, Ident, QuoteDelimitedString, Value, ValueWithSpan},
2525
dialect::OracleDialect,
2626
tokenizer::Span,
2727
};
@@ -33,6 +33,15 @@ fn oracle() -> TestedDialects {
3333
TestedDialects::new(vec![Box::new(OracleDialect)])
3434
}
3535

36+
/// Convenience constructor for [QuoteDelimitedstring].
37+
fn qds(start_quote: char, value: &'static str, end_quote: char) -> QuoteDelimitedString {
38+
QuoteDelimitedString {
39+
start_quote,
40+
value: value.into(),
41+
end_quote,
42+
}
43+
}
44+
3645
/// Oracle: `||` has a lower precedence than `*` and `/`
3746
#[test]
3847
fn muldiv_have_higher_precedence_than_strconcat() {
@@ -120,60 +129,56 @@ fn parse_quote_delimited_string() {
120129
let select = oracle().verified_only_select(sql);
121130
assert_eq!(10, select.projection.len());
122131
assert_eq!(
123-
&Expr::Value(Value::QuoteDelimitedStringLiteral('.', "abc".into(), '.').with_empty_span()),
132+
&Expr::Value(Value::QuoteDelimitedStringLiteral(qds('.', "abc", '.')).with_empty_span()),
124133
expr_from_projection(&select.projection[0])
125134
);
126135
assert_eq!(
127-
&Expr::Value(
128-
(Value::QuoteDelimitedStringLiteral('X', "ab'c".into(), 'X')).with_empty_span()
129-
),
136+
&Expr::Value((Value::QuoteDelimitedStringLiteral(qds('X', "ab'c", 'X'))).with_empty_span()),
130137
expr_from_projection(&select.projection[1])
131138
);
132139
assert_eq!(
133140
&Expr::Value(
134-
(Value::QuoteDelimitedStringLiteral('|', "abc'''".into(), '|')).with_empty_span()
141+
(Value::QuoteDelimitedStringLiteral(qds('|', "abc'''", '|'))).with_empty_span()
135142
),
136143
expr_from_projection(&select.projection[2])
137144
);
138145
assert_eq!(
139146
&Expr::Value(
140-
(Value::QuoteDelimitedStringLiteral('{', "abc}d".into(), '}')).with_empty_span()
147+
(Value::QuoteDelimitedStringLiteral(qds('{', "abc}d", '}'))).with_empty_span()
141148
),
142149
expr_from_projection(&select.projection[3])
143150
);
144151
assert_eq!(
145152
&Expr::Value(
146-
(Value::QuoteDelimitedStringLiteral('[', "]abc[".into(), ']')).with_empty_span()
153+
(Value::QuoteDelimitedStringLiteral(qds('[', "]abc[", ']'))).with_empty_span()
147154
),
148155
expr_from_projection(&select.projection[4])
149156
);
150157
assert_eq!(
151-
&Expr::Value(
152-
(Value::QuoteDelimitedStringLiteral('<', "a'bc".into(), '>')).with_empty_span()
153-
),
158+
&Expr::Value((Value::QuoteDelimitedStringLiteral(qds('<', "a'bc", '>'))).with_empty_span()),
154159
expr_from_projection(&select.projection[5])
155160
);
156161
assert_eq!(
157162
&Expr::Value(
158-
(Value::QuoteDelimitedStringLiteral('<', "<<a'bc".into(), '>')).with_empty_span()
163+
(Value::QuoteDelimitedStringLiteral(qds('<', "<<a'bc", '>'))).with_empty_span()
159164
),
160165
expr_from_projection(&select.projection[6])
161166
);
162167
assert_eq!(
163168
&Expr::Value(
164-
(Value::QuoteDelimitedStringLiteral('(', "'abc'('abc".into(), ')')).with_empty_span()
169+
(Value::QuoteDelimitedStringLiteral(qds('(', "'abc'('abc", ')'))).with_empty_span()
165170
),
166171
expr_from_projection(&select.projection[7])
167172
);
168173
assert_eq!(
169174
&Expr::Value(
170-
(Value::QuoteDelimitedStringLiteral('(', "abc'def)".into(), ')')).with_empty_span()
175+
(Value::QuoteDelimitedStringLiteral(qds('(', "abc'def)", ')'))).with_empty_span()
171176
),
172177
expr_from_projection(&select.projection[8])
173178
);
174179
assert_eq!(
175180
&Expr::Value(
176-
(Value::QuoteDelimitedStringLiteral('(', "abc'def))".into(), ')')).with_empty_span()
181+
(Value::QuoteDelimitedStringLiteral(qds('(', "abc'def))", ')'))).with_empty_span()
177182
),
178183
expr_from_projection(&select.projection[9])
179184
);
@@ -186,7 +191,7 @@ fn parse_quote_delimited_string_lowercase() {
186191
assert_eq!(1, select.projection.len());
187192
assert_eq!(
188193
&Expr::Value(
189-
Value::QuoteDelimitedStringLiteral('!', "a'b'c!d".into(), '!').with_empty_span()
194+
Value::QuoteDelimitedStringLiteral(qds('!', "a'b'c!d", '!')).with_empty_span()
190195
),
191196
expr_from_projection(&select.projection[0])
192197
);
@@ -221,7 +226,7 @@ fn parse_national_quote_delimited_string() {
221226
assert_eq!(1, select.projection.len());
222227
assert_eq!(
223228
&Expr::Value(
224-
Value::NationalQuoteDelimitedStringLiteral('.', "abc".into(), '.').with_empty_span()
229+
Value::NationalQuoteDelimitedStringLiteral(qds('.', "abc", '.')).with_empty_span()
225230
),
226231
expr_from_projection(&select.projection[0])
227232
);
@@ -237,7 +242,7 @@ fn parse_national_quote_delimited_string_lowercase() {
237242
assert_eq!(1, select.projection.len());
238243
assert_eq!(
239244
&Expr::Value(
240-
Value::NationalQuoteDelimitedStringLiteral('!', "a'b'c!d".into(), '!')
245+
Value::NationalQuoteDelimitedStringLiteral(qds('!', "a'b'c!d", '!'))
241246
.with_empty_span()
242247
),
243248
expr_from_projection(&select.projection[0])

0 commit comments

Comments
 (0)