use crate::types::AttributeValue;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ParseError {
#[error("{0}")]
Syntax(String),
#[error("{0}")]
Validation(String),
}
impl ParseError {
pub fn into_message(self) -> String {
match self {
ParseError::Syntax(m) => {
format!("Statement wasn't well formed, can't be processed: {m}")
}
ParseError::Validation(m) => m,
}
}
}
impl From<String> for ParseError {
fn from(message: String) -> Self {
ParseError::Syntax(message)
}
}
impl From<&str> for ParseError {
fn from(message: &str) -> Self {
ParseError::Syntax(message.to_string())
}
}
#[derive(Debug, Clone)]
pub enum Statement {
#[non_exhaustive]
Select {
table_name: String,
index_name: Option<String>,
projections: Vec<String>, where_clause: Option<WhereClause>,
order_by: Vec<OrderByTerm>,
},
#[non_exhaustive]
Insert {
table_name: String,
item: HashMap<String, PartiqlValue>,
if_not_exists: bool,
},
#[non_exhaustive]
Update {
table_name: String,
index_name: Option<String>,
set_clauses: Vec<SetClause>,
remove_paths: Vec<String>,
where_clause: Option<WhereClause>,
returning: Option<ReturningVariant>,
},
#[non_exhaustive]
Delete {
table_name: String,
index_name: Option<String>,
where_clause: Option<WhereClause>,
returning: Option<ReturningVariant>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct OrderByTerm {
pub path: String,
pub descending: bool,
pub rejection: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReturningVariant {
AllOld,
ModifiedOld,
AllNew,
ModifiedNew,
}
impl ReturningVariant {
pub fn as_sql(self) -> &'static str {
match self {
ReturningVariant::AllOld => "ALL OLD",
ReturningVariant::ModifiedOld => "MODIFIED OLD",
ReturningVariant::AllNew => "ALL NEW",
ReturningVariant::ModifiedNew => "MODIFIED NEW",
}
}
}
pub fn table_name(stmt: &Statement) -> Option<&str> {
match stmt {
Statement::Select { table_name, .. }
| Statement::Insert { table_name, .. }
| Statement::Update { table_name, .. }
| Statement::Delete { table_name, .. } => Some(table_name),
}
}
pub fn index_name(stmt: &Statement) -> Option<&str> {
match stmt {
Statement::Select { index_name, .. }
| Statement::Update { index_name, .. }
| Statement::Delete { index_name, .. } => index_name.as_deref(),
Statement::Insert { .. } => None,
}
}
pub fn returning_variant(stmt: &Statement) -> Option<ReturningVariant> {
match stmt {
Statement::Update { returning, .. } | Statement::Delete { returning, .. } => *returning,
_ => None,
}
}
pub(crate) fn select_target(input: &str) -> Option<(String, Option<String>)> {
let mut t = Tokenizer::new(input).ok()?;
if !is_keyword(&t.next_token().ok()??, "SELECT") {
return None;
}
while !is_keyword(&t.next_token().ok()??, "FROM") {}
let target = parse_table_name(&mut t).ok()?;
Some((target.name, target.index))
}
pub(crate) fn count_projection_rejection(statement: &str) -> Option<String> {
fn advance(c: char, line: &mut usize, col: &mut usize) {
if c == '\n' {
*line += 1;
*col = 1;
} else {
*col += 1;
}
}
let chars: Vec<char> = statement.chars().collect();
let len = chars.len();
let mut i = 0;
let mut line = 1usize;
let mut col = 1usize;
let mut first_word_seen = false;
while i < len {
let c = chars[i];
if c == '\'' || c == '"' {
let quote = c;
advance(c, &mut line, &mut col);
i += 1;
while i < len {
let ch = chars[i];
advance(ch, &mut line, &mut col);
i += 1;
if ch == quote {
if i < len && chars[i] == quote {
advance(chars[i], &mut line, &mut col);
i += 1; } else {
break;
}
}
}
continue;
}
if c.is_ascii_alphabetic() || c == '_' {
let word_line = line;
let word_col = col;
let start = i;
while i < len && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
advance(chars[i], &mut line, &mut col);
i += 1;
}
let word: String = chars[start..i].iter().collect();
if !first_word_seen {
first_word_seen = true;
if !word.eq_ignore_ascii_case("SELECT") {
return None;
}
continue;
}
if word.eq_ignore_ascii_case("FROM") {
return None;
}
let mut j = i;
while j < len && chars[j].is_ascii_whitespace() {
j += 1;
}
if j < len && chars[j] == '(' {
let length = word.chars().count();
return Some(format!(
"Unexpected path component at {word_line}:{word_col}:{length}"
));
}
continue;
}
advance(c, &mut line, &mut col);
i += 1;
}
None
}
#[derive(Debug, Clone)]
pub struct SetClause {
pub path: String,
pub value: SetValue,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SetValue {
Simple(PartiqlValue),
Add(String, PartiqlValue),
Sub(String, PartiqlValue),
ListAppend(PartiqlValue, PartiqlValue),
SetCall {
delete: bool,
path: String,
members: PartiqlValue,
refusal_at: String,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct WhereClause {
pub groups: Vec<Vec<Arc<WhereCondition>>>,
pub(crate) top_level_equalities: Vec<Condition>,
pub(crate) tree: Option<ConditionTree>,
}
#[derive(Debug, Clone)]
pub(crate) enum ConditionTree {
Leaf(Arc<WhereCondition>),
And(Vec<ConditionTree>),
Or(Vec<ConditionTree>),
}
impl ConditionTree {
fn of(expr: BoolExpr) -> Self {
match expr {
BoolExpr::Leaf(cond) => ConditionTree::Leaf(Arc::new(cond)),
BoolExpr::And(terms) => ConditionTree::And(terms.into_iter().map(Self::of).collect()),
BoolExpr::Or(terms) => ConditionTree::Or(terms.into_iter().map(Self::of).collect()),
BoolExpr::Not(_) => {
unreachable!("push_not_down removes every Not before the tree is kept")
}
BoolExpr::NonBoolean(_) => {
ConditionTree::Leaf(Arc::new(WhereCondition::Exists(String::new())))
}
}
}
fn bytes(&self) -> usize {
match self {
ConditionTree::Leaf(cond) => condition_bytes(cond),
ConditionTree::And(terms) | ConditionTree::Or(terms) => terms
.iter()
.fold(0usize, |total, term| total.saturating_add(term.bytes())),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum WhereCondition {
Comparison(Condition),
NotComparison(Condition),
Exists(String),
NotExists(String),
BeginsWith(String, PartiqlValue),
NotBeginsWith(String, PartiqlValue),
Between(String, PartiqlValue, PartiqlValue),
NotBetween(String, PartiqlValue, PartiqlValue),
In(String, Vec<PartiqlValue>),
Contains(String, PartiqlValue),
NotContains(String, PartiqlValue),
IsMissing(String),
IsNotMissing(String),
IsType(String, TypeTest),
IsNotType(String, TypeTest),
Compare(Operand, CompOp, Operand),
CompareBetween(Operand, Operand, Operand),
CompareIn(Operand, Vec<Operand>),
AttributeType(Operand, PartiqlValue),
Not(Box<WhereCondition>),
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Operand {
Path(String),
Value(PartiqlValue),
Size(Box<Operand>),
Condition(Box<WhereCondition>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TypeTest {
Null,
Boolean,
Blob,
String,
Numeric,
Struct,
List,
}
impl TypeTest {
pub fn matches(self, value: &AttributeValue) -> bool {
matches!(
(self, value),
(TypeTest::Null, AttributeValue::NULL(_))
| (TypeTest::Boolean, AttributeValue::BOOL(_))
| (TypeTest::Blob, AttributeValue::B(_))
| (TypeTest::String, AttributeValue::S(_))
| (TypeTest::Numeric, AttributeValue::N(_))
| (TypeTest::Struct, AttributeValue::M(_))
| (TypeTest::List, AttributeValue::L(_))
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Condition {
pub path: String,
pub op: CompOp,
pub value: PartiqlValue,
}
pub use crate::expressions::condition::CompOp;
#[derive(Debug, Clone, PartialEq)]
pub enum PartiqlValue {
Literal(AttributeValue),
Parameter(usize), }
pub(crate) enum ParseFailure {
Statement(ParseError),
Literal(String),
}
impl From<ParseError> for ParseFailure {
fn from(error: ParseError) -> Self {
ParseFailure::Statement(error)
}
}
impl From<String> for ParseFailure {
fn from(message: String) -> Self {
ParseFailure::Statement(ParseError::Syntax(message))
}
}
impl From<&str> for ParseFailure {
fn from(message: &str) -> Self {
ParseFailure::Statement(ParseError::Syntax(message.to_string()))
}
}
#[cfg(test)]
pub(crate) fn parse(input: &str) -> Result<Statement, ParseError> {
parse_counting_placeholders(input)
.map(|(statement, _)| statement)
.map_err(|failure| match failure {
ParseFailure::Statement(error) => error,
ParseFailure::Literal(message) => ParseError::Validation(message),
})
}
pub(crate) const PARAMETER_COUNT_MISMATCH: &str =
"Number of parameters in request and statement don't match.";
pub(crate) fn parse_with_parameters(
input: &str,
supplied: usize,
) -> Result<Statement, ParseFailure> {
let (statement, placeholders) = parse_counting_placeholders(input)?;
if placeholders > supplied {
return Err(ParseFailure::Statement(ParseError::Validation(
PARAMETER_COUNT_MISMATCH.to_string(),
)));
}
Ok(statement)
}
fn parse_counting_placeholders(input: &str) -> Result<(Statement, usize), ParseFailure> {
let mut tokenizer = Tokenizer::new(input)?;
let first = tokenizer
.next_token()?
.ok_or("Empty statement")?
.to_uppercase();
let next = tokenizer.peek_token()?;
let statement = match first.as_str() {
"SELECT" if next.is_none() => Err(UNEXPECTED_TERM.into()),
"SELECT" => parse_select(&mut tokenizer),
"INSERT" if !next.as_deref().is_some_and(|tok| is_keyword(tok, "INTO")) => {
Err(UNEXPECTED_KEYWORD.into())
}
"INSERT" => parse_insert(&mut tokenizer),
"UPDATE"
if !next.as_deref().is_some_and(|tok| {
tok.starts_with('"')
|| tok.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
}) =>
{
Err(EXPECTED_SIMPLE_PATH.into())
}
"UPDATE" => parse_update(&mut tokenizer),
"DELETE" if !next.as_deref().is_some_and(|tok| is_keyword(tok, "FROM")) => {
Err("Expected FROM after DELETE".into())
}
"DELETE" => parse_delete(&mut tokenizer),
"FROM" if next.as_deref().is_some_and(is_name) => parse_from_first(&mut tokenizer),
_ => {
tokenizer.pos = 0;
Err(refuse_non_dml(&mut tokenizer)?)
}
}?;
end_of_statement(&mut tokenizer)?;
if let Some(failure) = tokenizer.first_refusal() {
return Err(failure);
}
Ok((statement, tokenizer.param_counter))
}
fn end_of_statement(t: &mut Tokenizer) -> Result<(), String> {
match t.peek_token()?.as_deref() {
None => Ok(()),
Some(";") => {
t.next_token()?;
if t.peek_token()?.is_some() {
Err("Unexpected token after semicolon. (Only one query is allowed.)".to_string())
} else {
Ok(())
}
}
Some(_) => Err("Unexpected token after expression".to_string()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExprKind {
Number,
Lit,
Missing,
List,
Struct,
Bag,
Parameter,
Id,
Path,
IsType,
SimpleCase,
SearchedCase,
Other,
}
type ExprError = &'static str;
const UNEXPECTED_TERM: ExprError = "Unexpected term";
const UNEXPECTED_KEYWORD: ExprError = "Unexpected keyword";
const UNEXPECTED_OPERATOR: ExprError = "Unexpected operator";
const MISSING_RIGHT_HAND_SIDE: ExprError = "Missing right-hand side expression of infix operator";
const EXPECTED_SIMPLE_PATH: ExprError = "Expected identifier for simple path";
type ExprResult = Result<Result<ExprKind, ExprError>, String>;
macro_rules! term {
($read:expr) => {
match $read? {
Ok(kind) => kind,
Err(e) => return Ok(Err(e)),
}
};
}
const OPERATOR_WORDS: &[&str] = &["AND", "OR", "NOT", "IS", "IN", "LIKE", "BETWEEN"];
const KEYWORDS: &[&str] = &[
"SELECT",
"FROM",
"WHERE",
"INSERT",
"INTO",
"VALUE",
"VALUES",
"UPDATE",
"DELETE",
"SET",
"REMOVE",
"ORDER",
"BY",
"GROUP",
"HAVING",
"LIMIT",
"AS",
"AT",
"ON",
"RETURNING",
"UNION",
"CASE",
"WHEN",
"THEN",
"ELSE",
"END",
"ASC",
"DESC",
"EXISTS",
"TRUE",
"FALSE",
"NULL",
"MISSING",
];
const CLAUSE_KEYWORDS: &[&str] = &[
"SELECT",
"FROM",
"WHERE",
"INSERT",
"INTO",
"VALUE",
"VALUES",
"UPDATE",
"DELETE",
"SET",
"REMOVE",
"ORDER",
"BY",
"GROUP",
"HAVING",
"LIMIT",
"AS",
"AT",
"ON",
"RETURNING",
"UNION",
"ASC",
"DESC",
];
enum TypeName {
Missing,
Test(TypeTest),
Unsupported(&'static str),
}
fn read_type_name(t: &mut Tokenizer) -> Result<Option<TypeName>, String> {
use TypeName::{Missing, Test, Unsupported};
let Some(word) = t.next_token()? else {
return Ok(None);
};
let mut then = |next: &str| -> Result<bool, String> {
let follows = t.peek_token()?.is_some_and(|tok| is_keyword(&tok, next));
if follows {
t.next_token()?;
}
Ok(follows)
};
let name = match word.to_ascii_uppercase().as_str() {
"NULL" => Test(TypeTest::Null),
"MISSING" => Missing,
"BOOL" | "BOOLEAN" => Test(TypeTest::Boolean),
"BLOB" => Test(TypeTest::Blob),
"STRING" => Test(TypeTest::String),
"NUMERIC" => Test(TypeTest::Numeric),
"STRUCT" | "TUPLE" => Test(TypeTest::Struct),
"LIST" => Test(TypeTest::List),
"SMALLINT" => Unsupported("SmallintType"),
"INT" | "INTEGER" => Unsupported("IntegerType"),
"INT4" => Unsupported("Integer4Type"),
"INT8" | "BIGINT" => Unsupported("Integer8Type"),
"FLOAT" => Unsupported("FloatType"),
"REAL" => Unsupported("RealType"),
"DOUBLE" if then("PRECISION")? => Unsupported("DoublePrecisionType"),
"DECIMAL" => Unsupported("DecimalType"),
"TIMESTAMP" => Unsupported("TimestampType"),
"DATE" => Unsupported("DateType"),
"TIME" => Unsupported("TimeType"),
"CHARACTER" if then("VARYING")? => Unsupported("CharacterVaryingType"),
"CHAR" | "CHARACTER" => Unsupported("CharacterType"),
"VARCHAR" => Unsupported("CharacterVaryingType"),
"SYMBOL" => Unsupported("SymbolType"),
"CLOB" => Unsupported("ClobType"),
"SEXP" => Unsupported("SexpType"),
"BAG" => Unsupported("BagType"),
"ANY" => Unsupported("AnyType"),
_ => return Ok(None),
};
if t.peek_token()?.as_deref() == Some("(") {
t.next_token()?;
loop {
match t.next_token()?.as_deref() {
Some(")") => break,
Some(tok) if tok == "," || is_number_token(tok) => {}
_ => return Ok(None),
}
}
}
Ok(Some(name))
}
fn is_keyword(tok: &str, word: &str) -> bool {
tok.eq_ignore_ascii_case(word)
}
fn is_any(tok: &str, words: &[&str]) -> bool {
words.iter().any(|w| is_keyword(tok, w))
}
const DOT_COMPONENT_KEYWORDS: &[&str] = &[
"absolute",
"action",
"add",
"all",
"allocate",
"alter",
"and",
"any",
"are",
"as",
"asc",
"assertion",
"at",
"authorization",
"avg",
"bag",
"begin",
"between",
"bigint",
"bit",
"bit_length",
"blob",
"bool",
"boolean",
"by",
"can_cast",
"can_lossless_cast",
"cascade",
"cascaded",
"case",
"cast",
"catalog",
"char",
"char_length",
"character",
"character_length",
"check",
"clob",
"close",
"coalesce",
"collate",
"collation",
"column",
"commit",
"conflict",
"connect",
"connection",
"constraint",
"constraints",
"continue",
"convert",
"corresponding",
"count",
"create",
"cross",
"current",
"current_date",
"current_time",
"current_timestamp",
"current_user",
"cursor",
"date",
"date_add",
"date_diff",
"deallocate",
"dec",
"decimal",
"declare",
"default",
"deferrable",
"deferred",
"delete",
"desc",
"describe",
"descriptor",
"diagnostics",
"disconnect",
"distinct",
"do",
"domain",
"double",
"drop",
"else",
"end",
"escape",
"except",
"exception",
"exec",
"execute",
"exists",
"external",
"extract",
"false",
"fetch",
"first",
"float",
"for",
"foreign",
"found",
"from",
"full",
"get",
"global",
"go",
"goto",
"grant",
"group",
"having",
"identity",
"immediate",
"in",
"index",
"indicator",
"initially",
"inner",
"input",
"insensitive",
"insert",
"int",
"integer",
"integer2",
"integer4",
"integer8",
"intersect",
"interval",
"into",
"is",
"isolation",
"join",
"key",
"language",
"last",
"left",
"let",
"level",
"like",
"limit",
"list",
"local",
"lower",
"match",
"max",
"min",
"missing",
"modified",
"module",
"names",
"national",
"natural",
"nchar",
"new",
"next",
"no",
"not",
"nothing",
"null",
"nullif",
"nulls",
"numeric",
"octet_length",
"of",
"offset",
"old",
"on",
"only",
"open",
"option",
"or",
"order",
"outer",
"output",
"overlaps",
"pad",
"partial",
"pivot",
"position",
"precision",
"prepare",
"preserve",
"primary",
"prior",
"privileges",
"procedure",
"public",
"read",
"real",
"references",
"relative",
"remove",
"restrict",
"returning",
"revoke",
"right",
"rollback",
"rows",
"schema",
"scroll",
"section",
"select",
"session",
"session_user",
"set",
"sexp",
"size",
"smallint",
"some",
"space",
"sql",
"sqlcode",
"sqlerror",
"sqlstate",
"string",
"struct",
"substring",
"sum",
"symbol",
"system_user",
"table",
"temporary",
"then",
"time",
"timestamp",
"to",
"transaction",
"translate",
"trim",
"true",
"tuple",
"union",
"unique",
"unknown",
"unpivot",
"update",
"upper",
"usage",
"user",
"using",
"value",
"values",
"varchar",
"varying",
"view",
"when",
"whenever",
"where",
"with",
"work",
"write",
"zone",
];
fn check_dot_component(segment: &str) -> Result<(), String> {
let lower = segment.to_ascii_lowercase();
if !segment.starts_with('"')
&& DOT_COMPONENT_KEYWORDS
.binary_search(&lower.as_str())
.is_ok()
{
return Err("Invalid path dot component".to_string());
}
Ok(())
}
fn is_name(tok: &str) -> bool {
tok.starts_with('"')
|| (tok.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
&& !is_any(tok, KEYWORDS)
&& !is_any(tok, OPERATOR_WORDS))
}
fn refuse_non_dml(t: &mut Tokenizer) -> Result<ParseError, String> {
if t.peek_token()?
.is_some_and(|first| is_keyword(&first, "SET") || is_keyword(&first, "REMOVE"))
{
if let Err(e) = read_tableless_dml(t)? {
return Ok(ParseError::Syntax(e.to_string()));
}
return Ok(match end_of_statement(t) {
Ok(()) => ParseError::Validation(
"Only select from a single table or index is supported.".to_string(),
),
Err(message) => ParseError::Syntax(message),
});
}
let kind = match read_expression(t)? {
Ok(kind) => kind,
Err(e) => return Ok(ParseError::Syntax(e.to_string())),
};
if let Err(message) = end_of_statement(t) {
return Ok(ParseError::Syntax(message));
}
let name = match kind {
ExprKind::Number | ExprKind::Lit => "Lit",
ExprKind::Missing => "Missing",
ExprKind::List => "List",
ExprKind::Struct => "Struct",
ExprKind::Bag => "Bag",
ExprKind::Parameter => "Parameter",
ExprKind::Id => "Id",
ExprKind::Path => "Path",
ExprKind::IsType => "IsType",
ExprKind::SimpleCase => "SimpleCase",
ExprKind::SearchedCase => "SearchedCase",
ExprKind::Other => {
return Ok(ParseError::Validation(
"EXISTS can only be used in ExecuteTransaction write requests.".to_string(),
));
}
};
Ok(ParseError::Validation(format!(
"Unsupported operation: {name}"
)))
}
fn read_tableless_dml(t: &mut Tokenizer) -> ExprResult {
let remove = t
.next_token()?
.is_some_and(|verb| is_keyword(&verb, "REMOVE"));
loop {
match t.next_token()? {
Some(name) if is_name(&name) => {}
_ => return Ok(Err(EXPECTED_SIMPLE_PATH)),
}
term!(read_path_steps(t, ExprKind::Id));
if remove {
return Ok(Ok(ExprKind::Other));
}
if t.next_token()?.as_deref() != Some("=") {
return Ok(Err("Expected '='"));
}
term!(read_expression(t));
if t.peek_token()?.as_deref() != Some(",") {
return Ok(Ok(ExprKind::Other));
}
t.next_token()?;
}
}
fn read_operand(t: &mut Tokenizer, read: fn(&mut Tokenizer) -> ExprResult) -> ExprResult {
if t.peek_token()?.is_none() {
return Ok(Err(MISSING_RIGHT_HAND_SIDE));
}
read(t)
}
fn nested(t: &mut Tokenizer, read: impl FnOnce(&mut Tokenizer) -> ExprResult) -> ExprResult {
if t.depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
t.depth += 1;
let result = read(t);
t.depth -= 1;
result
}
fn read_expression(t: &mut Tokenizer) -> ExprResult {
nested(t, |t| {
let mut kind = term!(read_and(t));
while t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "OR")) {
t.next_token()?;
term!(read_operand(t, read_and));
kind = ExprKind::Other;
}
Ok(Ok(kind))
})
}
fn read_and(t: &mut Tokenizer) -> ExprResult {
let mut kind = term!(read_not(t));
while t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "AND")) {
t.next_token()?;
term!(read_operand(t, read_not));
kind = ExprKind::Other;
}
Ok(Ok(kind))
}
fn read_not(t: &mut Tokenizer) -> ExprResult {
let mut negated = false;
while t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "NOT")) {
t.next_token()?;
negated = true;
}
let kind = term!(read_comparison(t));
Ok(Ok(if negated { ExprKind::Other } else { kind }))
}
fn read_comparison(t: &mut Tokenizer) -> ExprResult {
let kind = term!(read_additive(t));
let Some(next) = t.peek_token()? else {
return Ok(Ok(kind));
};
let closes_bag = next == ">" && t.peek_token_at(1)?.as_deref() == Some(">");
if !closes_bag && matches!(next.as_str(), "=" | "<>" | "!=" | "<" | "<=" | ">" | ">=") {
t.next_token()?;
term!(read_operand(t, read_additive));
return Ok(Ok(ExprKind::Other));
}
if is_keyword(&next, "IS") {
t.next_token()?;
let negated = t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "NOT"));
if negated {
t.next_token()?;
}
return Ok(match read_type_name(t)? {
Some(_) => Ok(if negated {
ExprKind::Other
} else {
ExprKind::IsType
}),
None => Err("Expected type name"),
});
}
let negated = is_keyword(&next, "NOT");
let operator = if negated {
t.peek_token_at(1)?
} else {
Some(next)
};
let Some(operator) = operator else {
return Ok(Ok(kind));
};
if !is_any(&operator, &["IN", "LIKE", "BETWEEN"]) {
return Ok(Ok(kind));
}
if negated {
t.next_token()?;
}
t.next_token()?;
term!(read_operand(t, read_additive));
if is_keyword(&operator, "LIKE")
&& t.peek_token()?
.is_some_and(|tok| is_keyword(&tok, "ESCAPE"))
{
t.next_token()?;
term!(read_operand(t, read_additive));
}
if is_keyword(&operator, "BETWEEN") {
match t.next_token()? {
Some(tok) if is_keyword(&tok, "AND") => {}
_ => return Ok(Err("Expected AND keyword")),
}
if t.peek_token()?.is_none() {
return Ok(Err("Expected expression after AND"));
}
term!(read_additive(t));
}
Ok(Ok(ExprKind::Other))
}
fn read_additive(t: &mut Tokenizer) -> ExprResult {
let mut kind = term!(read_multiplicative(t));
while t.peek_token()?.is_some_and(|tok| tok == "+" || tok == "-") {
t.next_token()?;
term!(read_operand(t, read_multiplicative));
kind = ExprKind::Other;
}
Ok(Ok(kind))
}
fn read_multiplicative(t: &mut Tokenizer) -> ExprResult {
let mut kind = term!(read_unary(t));
while t.peek_token()?.is_some_and(|tok| tok == "*") {
t.next_token()?;
term!(read_operand(t, read_unary));
kind = ExprKind::Other;
}
Ok(Ok(kind))
}
fn read_unary(t: &mut Tokenizer) -> ExprResult {
let (mut negated, mut signed) = (false, false);
loop {
match t.peek_token()? {
Some(tok) if is_keyword(&tok, "NOT") => negated = true,
Some(tok) if tok == "-" || tok == "+" => signed = true,
_ => break,
}
t.next_token()?;
}
let kind = term!(read_primary(t));
Ok(Ok(match kind {
_ if negated => ExprKind::Other,
ExprKind::Number => ExprKind::Number,
_ if signed => ExprKind::Other,
kind => kind,
}))
}
fn read_elements(
t: &mut Tokenizer,
close: &[&str],
missing: ExprError,
) -> Result<Result<(), ExprError>, String> {
let closes = |t: &Tokenizer| -> Result<bool, String> {
Ok(close
.iter()
.enumerate()
.all(|(i, c)| t.peek_token_at(i).ok().flatten().as_deref() == Some(*c)))
};
let consume = |t: &mut Tokenizer| -> Result<(), String> {
for _ in close {
t.next_token()?;
}
Ok(())
};
if closes(t)? {
return Ok(Ok(consume(t)?));
}
loop {
if let Err(e) = read_expression(t)? {
return Ok(Err(e));
}
if t.peek_token()?.as_deref() == Some(",") {
t.next_token()?;
continue;
}
if closes(t)? {
return Ok(Ok(consume(t)?));
}
return Ok(Err(missing));
}
}
fn read_subquery(t: &mut Tokenizer) -> ExprResult {
if t.peek_token()?.is_none() {
return Ok(Err(UNEXPECTED_TERM));
}
let mut depth = 0usize;
while let Some(tok) = t.peek_token()? {
match tok.as_str() {
"(" => depth += 1,
")" if depth == 0 => break,
")" => depth -= 1,
_ => {}
}
t.next_token()?;
}
Ok(Ok(ExprKind::Other))
}
fn read_parenthesised(t: &mut Tokenizer) -> ExprResult {
let kind = if t
.peek_token()?
.is_some_and(|tok| is_keyword(&tok, "SELECT"))
{
t.next_token()?;
term!(read_subquery(t))
} else {
term!(read_expression(t))
};
if t.next_token()?.as_deref() != Some(")") {
return Ok(Err("Expected RIGHT_PAREN"));
}
Ok(Ok(kind))
}
fn read_case(t: &mut Tokenizer) -> ExprResult {
let Some(next) = t.peek_token()? else {
return Ok(Err(UNEXPECTED_TERM));
};
let searched = is_keyword(&next, "WHEN");
if !searched {
term!(read_expression(t));
}
let mut arms = 0;
while t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "WHEN")) {
t.next_token()?;
term!(read_expression(t));
if !t.next_token()?.is_some_and(|tok| is_keyword(&tok, "THEN")) {
return Ok(Err("Expected THEN keyword"));
}
term!(read_expression(t));
arms += 1;
}
if arms == 0 {
return Ok(Err("Expected a WHEN clause in CASE"));
}
if t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "ELSE")) {
t.next_token()?;
term!(read_expression(t));
}
if !t.next_token()?.is_some_and(|tok| is_keyword(&tok, "END")) {
return Ok(Err("Expected END keyword"));
}
Ok(Ok(if searched {
ExprKind::SearchedCase
} else {
ExprKind::SimpleCase
}))
}
fn read_path_steps(t: &mut Tokenizer, kind: ExprKind) -> ExprResult {
let mut kind = kind;
loop {
match t.peek_token()?.as_deref() {
Some(".") => {
t.next_token()?;
match t.next_token()? {
Some(step)
if step == "*"
|| step.starts_with('"')
|| step.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') => {}
_ => return Ok(Err("Invalid path dot component")),
}
}
Some("[") => {
t.next_token()?;
if t.peek_token()?.as_deref() == Some("*") {
t.next_token()?;
} else {
term!(read_expression(t));
}
if t.next_token()?.as_deref() != Some("]") {
return Ok(Err("Expected RIGHT_BRACKET"));
}
}
_ => return Ok(Ok(kind)),
}
kind = ExprKind::Path;
}
}
fn read_primary(t: &mut Tokenizer) -> ExprResult {
let Some(tok) = t.next_token()? else {
return Ok(Err(UNEXPECTED_TERM));
};
if is_number_token(&tok) {
return Ok(Ok(ExprKind::Number));
}
let kind = match tok.as_str() {
_ if tok.starts_with('\'') && tok.len() >= 2 => ExprKind::Lit,
_ if is_any(&tok, &["TRUE", "FALSE", "NULL"]) => ExprKind::Lit,
_ if is_keyword(&tok, "MISSING") => ExprKind::Missing,
"?" => ExprKind::Parameter,
"[" => {
if let Err(e) = read_elements(t, &["]"], "Expected RIGHT_BRACKET")? {
return Ok(Err(e));
}
ExprKind::List
}
"<" if t.peek_token()?.as_deref() == Some("<>")
&& t.peek_token_at(1)?.as_deref() == Some(">") =>
{
t.next_token()?;
t.next_token()?;
ExprKind::Bag
}
"<" if t.peek_token()?.as_deref() == Some("<") => {
t.next_token()?;
if let Err(e) = read_elements(t, &[">", ">"], "Expected RIGHT_DOUBLE_ANGLE_BRACKET")? {
return Ok(Err(e));
}
ExprKind::Bag
}
"(" => term!(read_parenthesised(t)),
"{" => {
if t.peek_token()?.as_deref() == Some("}") {
t.next_token()?;
} else {
loop {
term!(read_expression(t));
if t.next_token()?.as_deref() != Some(":") {
return Ok(Err("Expected COLON"));
}
term!(read_expression(t));
match t.next_token()?.as_deref() {
Some(",") => {}
Some("}") => break,
_ => return Ok(Err("Expected RIGHT_CURLY")),
}
}
}
ExprKind::Struct
}
_ if is_keyword(&tok, "CASE") => return read_case(t),
_ if is_keyword(&tok, "SELECT") => return read_subquery(t),
_ if is_keyword(&tok, "EXISTS") => {
if t.peek_token()?.as_deref() != Some("(") {
return Ok(Err(UNEXPECTED_KEYWORD));
}
t.next_token()?;
term!(read_parenthesised(t));
return Ok(Ok(ExprKind::Other));
}
_ if is_keyword(&tok, "FROM") => {
return Ok(Err(if t.peek_token()?.is_some_and(|next| is_name(&next)) {
"Expected data manipulation"
} else {
UNEXPECTED_TERM
}));
}
"=" | "<>" | "!=" | "<" | "<=" | ">" | ">=" => return Ok(Err(UNEXPECTED_OPERATOR)),
_ if is_any(&tok, OPERATOR_WORDS) => return Ok(Err(UNEXPECTED_OPERATOR)),
_ if is_any(&tok, KEYWORDS) => return Ok(Err(UNEXPECTED_KEYWORD)),
_ if is_name(&tok) => {
if t.peek_token()?.as_deref() == Some("(") {
t.next_token()?;
if let Err(e) = read_elements(t, &[")"], "Expected RIGHT_PAREN")? {
return Ok(Err(e));
}
ExprKind::Other
} else {
ExprKind::Id
}
}
_ => return Ok(Err(UNEXPECTED_TERM)),
};
read_path_steps(t, kind)
}
fn parse_order_by(t: &mut Tokenizer) -> Result<Vec<OrderByTerm>, String> {
expect_keyword(t, "ORDER")?;
expect_keyword(t, "BY")?;
let mut terms = Vec::new();
let mut rejected = false;
loop {
let span = t.span_at(t.pos);
let tok = t
.next_token()?
.ok_or("Expected attribute path in ORDER BY")?;
if tok.eq_ignore_ascii_case("ASC") || tok.eq_ignore_ascii_case("DESC") {
return Err(format!(
"Expected attribute path in ORDER BY but got '{tok}'"
));
}
let found = if tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2 {
Some(tok[1..tok.len() - 1].to_string())
} else if matches!(t.peek_token()?.as_deref(), Some(".") | Some("[")) {
Some("PATH".to_string())
} else {
None
};
let rejection = found.filter(|_| !rejected).map(|found| {
format!(
"Argument to ORDER BY must be of type VariableReference. Type: {found} at {}",
t.position(span)
)
});
rejected |= rejection.is_some();
let path = parse_dotted_path_from_token(&tok, t)?;
let mut descending = false;
if let Some(direction) = t
.peek_token()?
.filter(|s| s.eq_ignore_ascii_case("ASC") || s.eq_ignore_ascii_case("DESC"))
{
t.next_token()?; descending = direction.eq_ignore_ascii_case("DESC");
}
if t.peek_token()?
.is_some_and(|s| s.eq_ignore_ascii_case("NULLS"))
{
t.next_token()?; let which = t
.next_token()?
.ok_or("Expected FIRST or LAST after NULLS")?;
if !(which.eq_ignore_ascii_case("FIRST") || which.eq_ignore_ascii_case("LAST")) {
return Err(format!(
"Expected FIRST or LAST after NULLS but got '{which}'"
));
}
}
terms.push(OrderByTerm {
path,
descending,
rejection,
});
match t.peek_token()? {
Some(ref s) if s == "," => {
t.next_token()?; }
_ => return Ok(terms),
}
}
}
fn parse_select(t: &mut Tokenizer) -> Result<Statement, ParseError> {
let projections = parse_projections(t)?;
expect_keyword(t, "FROM")?;
match t.peek_token()?.as_deref() {
None | Some(";") => return Err(UNEXPECTED_TERM.into()),
Some(tok) if is_any(tok, KEYWORDS) => return Err(UNEXPECTED_KEYWORD.into()),
_ => {}
}
let target = parse_table_name(t)?;
reject_alias(t)?;
let where_clause = parse_optional_where(t)?;
let order_by = if t
.peek_token()?
.is_some_and(|tok| tok.eq_ignore_ascii_case("ORDER"))
{
parse_order_by(t)?
} else {
Vec::new()
};
Ok(Statement::Select {
table_name: target.name,
index_name: target.index,
projections,
where_clause,
order_by,
})
}
fn parse_projections(t: &mut Tokenizer) -> Result<Vec<String>, String> {
let tok = t.peek_token()?.ok_or("Expected projection")?;
if is_keyword(&tok, "FROM") {
return Err(
"Expected identifier or an expression but found unexpected keyword 'from' \
in a select list."
.to_string(),
);
}
if tok == "*" {
t.next_token()?; return Ok(Vec::new());
}
let mut projections = Vec::new();
loop {
let name = t
.next_token()?
.ok_or("Expected projection attribute name")?;
let mut path = path_segment(&name);
loop {
match t.peek_token()? {
Some(ref s) if s == "." => {
t.next_token()?; let segment = t.next_token()?.ok_or("Expected attribute name after '.'")?;
check_dot_component(&segment)?;
path.push('.');
path.push_str(&path_segment(&segment));
}
Some(ref s) if s == "[" => {
t.next_token()?; let idx = t.next_token()?.ok_or("Expected index in '[]'")?;
let close = t.next_token()?.ok_or("Expected ']'")?;
if close != "]" {
return Err(format!("Expected ']' but got '{close}'"));
}
path.push('[');
path.push_str(&idx);
path.push(']');
}
_ => break,
}
}
projections.push(path);
match t.peek_token()? {
Some(ref s) if s == "," => {
t.next_token()?; }
_ => break,
}
}
Ok(projections)
}
fn parse_insert(t: &mut Tokenizer) -> Result<Statement, ParseError> {
expect_keyword(t, "INTO")?;
if t.peek_token()?.is_none() {
return Err(EXPECTED_SIMPLE_PATH.into());
}
let target = parse_table_name(t)?;
if target.index.is_some() {
return Err(ParseError::Validation(format!(
"FROM clause may only contain a single table name in data \
manipulation statements at {}",
t.position(target.span)
)));
}
let table_name = target.name;
if let Some(tok) = t.peek_token()?
&& is_alias(&tok)
&& (tok.eq_ignore_ascii_case("AS")
|| t.peek_token_at(1)?
.is_some_and(|next| next.eq_ignore_ascii_case("VALUE")))
{
return Err("Unexpected token after expression".into());
}
expect_keyword(t, "VALUE")?;
t.clause = LiteralClause::Value;
let item = parse_item_literal_partiql(t, 0)?;
let if_not_exists = if let Some(ref tok) = t.peek_token()? {
if tok.eq_ignore_ascii_case("IF") {
t.next_token()?; expect_keyword(t, "NOT")?;
expect_keyword(t, "EXISTS")?;
true
} else {
false
}
} else {
false
};
Ok(Statement::Insert {
table_name,
item,
if_not_exists,
})
}
fn parse_update(t: &mut Tokenizer) -> Result<Statement, ParseError> {
let target = parse_table_name(t)?;
reject_alias(t)?;
let (set_clauses, remove_paths) = parse_update_clauses(t)?;
if set_clauses.is_empty() && remove_paths.is_empty() {
return Err("UPDATE requires at least one SET or REMOVE clause".into());
}
let where_clause = parse_optional_where(t)?;
let returning = parse_optional_returning(t)?;
Ok(Statement::Update {
table_name: target.name,
index_name: target.index,
set_clauses,
remove_paths,
where_clause,
returning,
})
}
fn parse_update_clauses(t: &mut Tokenizer) -> Result<(Vec<SetClause>, Vec<String>), String> {
let mut set_clauses = Vec::new();
let mut remove_paths = Vec::new();
while let Some(keyword) = t.peek_token()? {
let remove = if is_keyword(&keyword, "SET") {
false
} else if is_keyword(&keyword, "REMOVE") {
true
} else {
break;
};
t.next_token()?;
loop {
let path_tok = t.next_token()?.ok_or(EXPECTED_SIMPLE_PATH)?;
let path = parse_dotted_path_from_token(&path_tok, t)?;
if remove {
remove_paths.push(path);
} else {
let eq = t.next_token()?.ok_or("Expected '='")?;
if eq != "=" {
return Err(format!("Expected '=' but got '{eq}'"));
}
let value = parse_set_value(t, &path)?;
set_clauses.push(SetClause { path, value });
}
match t.peek_token()? {
Some(ref s) if s == "," => {
t.next_token()?;
}
_ => break,
}
}
}
Ok((set_clauses, remove_paths))
}
fn parse_from_first(t: &mut Tokenizer) -> Result<Statement, ParseError> {
const NO_VERB: &str = "Expected data manipulation";
let target = parse_table_name(t)?;
reject_alias(t)?;
let is_dml_keyword = |tok: Option<String>| {
tok.is_some_and(|tok| is_keyword(&tok, "SET") || is_keyword(&tok, "REMOVE"))
};
if t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "WHERE"))
&& is_dml_keyword(t.peek_token_at(1)?)
{
return Err(NO_VERB.into());
}
let where_clause = parse_optional_where(t)?;
if !is_dml_keyword(t.peek_token()?) {
return Err(NO_VERB.into());
}
let (set_clauses, remove_paths) = parse_update_clauses(t)?;
let returning = parse_optional_returning(t)?;
Ok(Statement::Update {
table_name: target.name,
index_name: target.index,
set_clauses,
remove_paths,
where_clause,
returning,
})
}
fn parse_set_value(t: &mut Tokenizer, target: &str) -> Result<SetValue, String> {
t.clause = LiteralClause::Set;
if let Some(tok) = t.peek_token()?
&& let Some(delete) = set_function(&tok)
&& t.peek_token_at(1)?.as_deref() == Some("(")
{
return parse_set_call(t, target, delete);
}
if let Some(ref tok) = t.peek_token()?
&& tok.eq_ignore_ascii_case("list_append")
{
t.next_token()?; expect_char(t, "(")?;
let first = parse_value(t, 0)?;
let comma = t.next_token()?.ok_or("Expected ',' in list_append")?;
if comma != "," {
return Err(format!("Expected ',' but got '{comma}'"));
}
let second = parse_value(t, 0)?;
expect_char(t, ")")?;
return Ok(SetValue::ListAppend(first, second));
}
let first = parse_value(t, 0)?;
match t.peek_token()? {
Some(ref s) if s == "+" => {
t.next_token()?; let second = parse_value(t, 0)?;
let attr_path = match &first {
PartiqlValue::Literal(AttributeValue::S(s)) => s.clone(),
_ => {
return Err(
"Expected attribute path on left side of '+' expression".to_string()
);
}
};
Ok(SetValue::Add(attr_path, second))
}
Some(ref s) if s == "-" => {
t.next_token()?; let second = parse_value(t, 0)?;
let attr_path = match &first {
PartiqlValue::Literal(AttributeValue::S(s)) => s.clone(),
_ => {
return Err(
"Expected attribute path on left side of '-' expression".to_string()
);
}
};
Ok(SetValue::Sub(attr_path, second))
}
_ => Ok(SetValue::Simple(first)),
}
}
fn set_function(tok: &str) -> Option<bool> {
if is_keyword(tok, "set_add") {
Some(false)
} else if is_keyword(tok, "set_delete") {
Some(true)
} else {
None
}
}
fn parse_set_call(t: &mut Tokenizer, target: &str, delete: bool) -> Result<SetValue, String> {
let name = if delete { "SET_DELETE" } else { "SET_ADD" };
let call = t.pos;
t.next_token()?; t.next_token()?; let mut args: Vec<(usize, Result<String, PartiqlValue>)> = Vec::new();
if t.peek_token()?.as_deref() == Some(")") {
t.next_token()?;
} else {
loop {
let start = t.pos;
let tok = t.next_token()?.ok_or(UNEXPECTED_TERM)?;
let arg = if is_name(&tok) {
Ok(parse_dotted_path_from_token(&tok, t)?)
} else {
t.pos -= 1;
Err(parse_value(t, 0)?)
};
args.push((start, arg));
match t.next_token()?.as_deref() {
Some(",") => continue,
Some(")") => break,
Some(other) => return Err(format!("Expected ')' but got '{other}'")),
None => return Err("Expected ')'".to_string()),
}
}
}
if t.peek_token()?.is_some_and(|tok| tok == "+" || tok == "-") {
t.note_unrecognized_function(call);
t.next_token()?;
parse_value(t, 0)?;
return Ok(SetValue::Simple(PartiqlValue::Literal(
AttributeValue::NULL(true),
)));
}
let call_at = t.position(t.span_at(call));
let first_at = args
.first()
.map(|(start, _)| t.position(t.span_at(*start)))
.unwrap_or_default();
let mut members = PartiqlValue::Literal(AttributeValue::NULL(true));
match args.as_slice() {
[(_, first), (_, second)] => {
if first.as_deref() != Ok(target) {
t.note_operand_refusal(format!(
"The first argument to {name} must equal the assignment value at {call_at}"
));
} else if !matches!(
second,
Err(PartiqlValue::Parameter(_))
| Err(PartiqlValue::Literal(
AttributeValue::SS(_) | AttributeValue::NS(_) | AttributeValue::BS(_)
))
) {
t.note_operand_refusal(format!(
"The second argument to {name} must be a value with type SET at {first_at}"
));
}
if let Err(value) = second {
members = value.clone();
}
}
_ => t.note_operand_refusal(format!(
"{name} must have exactly two arguments at {call_at}"
)),
}
Ok(SetValue::SetCall {
delete,
path: target.to_string(),
members,
refusal_at: first_at,
})
}
fn parse_delete(t: &mut Tokenizer) -> Result<Statement, ParseError> {
expect_keyword(t, "FROM")?;
if !t.peek_token()?.as_deref().is_some_and(is_name) {
return Err(EXPECTED_SIMPLE_PATH.into());
}
let target = parse_table_name(t)?;
reject_alias(t)?;
let where_clause = parse_optional_where(t)?;
let returning = parse_optional_returning(t)?;
Ok(Statement::Delete {
table_name: target.name,
index_name: target.index,
where_clause,
returning,
})
}
fn parse_optional_returning(t: &mut Tokenizer) -> Result<Option<ReturningVariant>, String> {
match t.peek_token()? {
Some(ref s) if s.eq_ignore_ascii_case("RETURNING") => {
t.next_token()?; let first = t
.next_token()?
.ok_or("Expected returning variant after RETURNING")?;
let second = t
.next_token()?
.ok_or("Expected returning variant after RETURNING")?;
let star = t.next_token()?.ok_or("Expected '*' in RETURNING clause")?;
if star != "*" || t.peek_token()?.is_some_and(|tok| tok != ";") {
return Err(
"Malformed RETURNING clause; expected RETURNING <ALL|MODIFIED> <OLD|NEW> *"
.to_string(),
);
}
let variant = match (
first.to_ascii_uppercase().as_str(),
second.to_ascii_uppercase().as_str(),
) {
("ALL", "OLD") => ReturningVariant::AllOld,
("MODIFIED", "OLD") => ReturningVariant::ModifiedOld,
("ALL", "NEW") => ReturningVariant::AllNew,
("MODIFIED", "NEW") => ReturningVariant::ModifiedNew,
_ => {
return Err(format!(
"Unsupported RETURNING clause: RETURNING {} {} *",
first.to_ascii_uppercase(),
second.to_ascii_uppercase()
));
}
};
Ok(Some(variant))
}
_ => Ok(None),
}
}
struct TableTarget {
name: String,
index: Option<String>,
span: Span,
}
fn parse_table_name(t: &mut Tokenizer) -> Result<TableTarget, ParseError> {
let span = t.span_at(t.pos);
let raw = t.next_token()?.ok_or("Expected table name")?;
let name = unquote(&raw);
if name.is_empty() {
return Err(ParseError::Validation(
"Path component cannot be an empty string".to_string(),
));
}
let index = match t.peek_token()? {
Some(ref dot) if dot == "." => {
t.next_token()?; let raw = t.next_token()?.ok_or("Expected index name after '.'")?;
let index = unquote(&raw);
if index.is_empty() {
return Err(ParseError::Validation(
"Path component cannot be an empty string".to_string(),
));
}
if matches!(t.peek_token()?, Some(ref dot) if dot == ".") {
return Err(ParseError::Validation(
"A path may contain at most 2 components in the FROM clause".to_string(),
));
}
Some(index)
}
_ => None,
};
Ok(TableTarget { name, index, span })
}
fn reject_alias(t: &Tokenizer) -> Result<(), ParseError> {
if t.peek_token()?.is_some_and(|tok| is_alias(&tok)) {
return Err(ParseError::Validation(
"Aliasing is not supported".to_string(),
));
}
Ok(())
}
fn is_alias(tok: &str) -> bool {
const NOT_AN_ALIAS: &[&str] = &[
"WHERE",
"SET",
"REMOVE",
"ORDER",
"RETURNING",
"VALUE",
"VALUES",
"LIMIT",
"OFFSET",
"GROUP",
"HAVING",
"UNION",
"INTERSECT",
"EXCEPT",
"JOIN",
"INNER",
"LEFT",
"RIGHT",
"OUTER",
"CROSS",
"ON",
"AT",
"LET",
"AND",
"OR",
"NOT",
"IF",
];
let bare_word = tok
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
bare_word
&& (tok.eq_ignore_ascii_case("AS")
|| !NOT_AN_ALIAS.iter().any(|kw| tok.eq_ignore_ascii_case(kw)))
}
fn parse_optional_where(t: &mut Tokenizer) -> Result<Option<WhereClause>, String> {
match t.peek_token()? {
Some(ref s) if s.eq_ignore_ascii_case("WHERE") => {
t.next_token()?; t.clause = LiteralClause::Where;
let parsed = parse_conditions_with_or(t)?;
Ok(Some(WhereClause {
groups: parsed.groups,
top_level_equalities: parsed.top_level_equalities,
tree: Some(parsed.tree),
}))
}
_ => Ok(None),
}
}
fn parse_conditions_with_or(t: &mut Tokenizer) -> Result<ParsedConditions, String> {
t.reading_where = true;
let expr = parse_or_expr(t, 0);
t.reading_where = false;
let expr = expr?;
if let Some((operator, kind)) = non_boolean(&expr, None) {
t.note_operand_refusal(match operator {
Some(operator) => format!(
"Incorrect operand type for operator or function; operator or function: \
{operator}, operand type: {kind}"
),
None => "WHERE clause must contain an expression that resolves to a boolean".into(),
});
}
let mut top_level_equalities = Vec::new();
collect_top_level_equalities(&expr, &mut top_level_equalities);
let refused = || ParsedConditions {
groups: Vec::new(),
top_level_equalities: Vec::new(),
tree: ConditionTree::And(Vec::new()),
};
if t.holds_refusal() {
return Ok(refused());
}
let flattened = push_not_down(expr, false).and_then(|expr| {
let tree = ConditionTree::of(expr);
if tree.bytes() > MAX_CLAUSE_BYTES {
return Err(TOO_COMPLEX.to_string());
}
let groups = to_dnf(&tree)?;
Ok((tree, groups))
});
let (tree, groups) = flattened?;
Ok(ParsedConditions {
groups,
top_level_equalities,
tree,
})
}
struct ParsedConditions {
groups: Vec<Vec<Arc<WhereCondition>>>,
top_level_equalities: Vec<Condition>,
tree: ConditionTree,
}
fn collect_top_level_equalities(expr: &BoolExpr, out: &mut Vec<Condition>) {
match expr {
BoolExpr::And(terms) => terms
.iter()
.for_each(|term| collect_top_level_equalities(term, out)),
BoolExpr::Leaf(WhereCondition::Comparison(cond)) if cond.op == CompOp::Eq => {
out.push(cond.clone())
}
_ => {}
}
}
fn non_boolean(
expr: &BoolExpr,
operator: Option<&'static str>,
) -> Option<(Option<&'static str>, &'static str)> {
match expr {
BoolExpr::NonBoolean(kind) => Some((operator, kind)),
BoolExpr::And(terms) => terms.iter().find_map(|e| non_boolean(e, Some("AND"))),
BoolExpr::Or(terms) => terms.iter().find_map(|e| non_boolean(e, Some("OR"))),
BoolExpr::Not(inner) => non_boolean(inner, Some("NOT")),
BoolExpr::Leaf(_) => None,
}
}
enum BoolExpr {
Leaf(WhereCondition),
And(Vec<BoolExpr>),
Or(Vec<BoolExpr>),
Not(Box<BoolExpr>),
NonBoolean(&'static str),
}
fn parse_or_expr(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
let mut terms = vec![parse_and_expr(t, depth)?];
while matches!(t.peek_token()?, Some(ref s) if s.eq_ignore_ascii_case("OR")) {
t.next_token()?;
t.count_operator();
require_right_hand_side(t)?;
terms.push(parse_and_expr(t, depth)?);
}
Ok(if terms.len() == 1 {
terms.pop().unwrap()
} else {
BoolExpr::Or(terms)
})
}
fn parse_and_expr(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
let mut terms = vec![parse_not_expr(t, depth)?];
while matches!(t.peek_token()?, Some(ref s) if s.eq_ignore_ascii_case("AND")) {
t.next_token()?;
t.count_operator();
require_right_hand_side(t)?;
terms.push(parse_not_expr(t, depth)?);
}
Ok(if terms.len() == 1 {
terms.pop().unwrap()
} else {
BoolExpr::And(terms)
})
}
fn require_right_hand_side(t: &mut Tokenizer) -> Result<(), String> {
match t.peek_token()? {
None => Err(MISSING_RIGHT_HAND_SIDE.to_string()),
Some(ref s) if s == ";" => Err(MISSING_RIGHT_HAND_SIDE.to_string()),
Some(_) => Ok(()),
}
}
fn parse_not_expr(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
let mut nots = 0usize;
while matches!(t.peek_token()?, Some(ref s) if s.eq_ignore_ascii_case("NOT")) {
let next = t.peek_token_at(1)?.unwrap_or_default();
if next.eq_ignore_ascii_case("BEGINS_WITH") {
break;
}
t.next_token()?;
nots += 1;
}
let mut expr = parse_primary(t, depth)?;
let wraps = match nots {
0 => 0,
n if n % 2 == 1 => 1,
_ => 2,
};
for _ in 0..wraps {
expr = BoolExpr::Not(Box::new(expr));
}
Ok(expr)
}
fn parse_primary(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
if matches!(t.peek_token()?, Some(ref s) if s == "(") {
t.next_token()?;
let inner = parse_or_expr(t, depth + 1)?;
match t.next_token()? {
Some(ref s) if s == ")" => Ok(inner),
other => Err(format!(
"Expected ')' but got '{}'",
other.unwrap_or_else(|| "end of statement".to_string())
)),
}
} else {
parse_condition(t, depth)
}
}
const WHERE_FUNCTIONS: &[&str] = &[
"attribute_exists",
"attribute_not_exists",
"begins_with",
"contains",
"size",
"attribute_type",
];
const ATTRIBUTE_TYPES: &[&str] = &["N", "BS", "L", "B", "NULL", "M", "S", "SS", "NS", "BOOL"];
const ATTRIBUTE_TYPE_OPERAND_TYPES: &str = "{S,HDS,N,FS,BS,NULL,DOUBLE,NS,DECIMAL,SS,DECIMALSET,\
BOOL,HD,DICT,INTSET,M,L,B,FLOAT,INT,DOUBLESET}";
const MISSING_COMPARED: &str = "MISSING must be compared using IS operator";
fn is_comparison_operator(tok: &str) -> bool {
matches!(tok, "=" | "<>" | "!=" | "<" | "<=" | ">" | ">=")
}
fn comparison_operator(tok: &str) -> Result<CompOp, String> {
Ok(match tok {
"=" => CompOp::Eq,
"<>" | "!=" => CompOp::Ne,
"<" => CompOp::Lt,
"<=" => CompOp::Le,
">" => CompOp::Gt,
">=" => CompOp::Ge,
other => return Err(format!("Unknown operator: {other}")),
})
}
fn mirrored(op: CompOp) -> CompOp {
match op {
CompOp::Lt => CompOp::Gt,
CompOp::Le => CompOp::Ge,
CompOp::Gt => CompOp::Lt,
CompOp::Ge => CompOp::Le,
other => other,
}
}
struct ConditionFunction {
holds: WhereCondition,
}
impl ConditionFunction {
fn condition(&self, holds: bool) -> Result<BoolExpr, String> {
if holds {
Ok(BoolExpr::Leaf(self.holds.clone()))
} else {
negate_condition(self.holds.clone())
}
}
fn operand(&self) -> Operand {
Operand::Condition(Box::new(self.holds.clone()))
}
fn compared(&self, t: &mut Tokenizer, op: &str, value: Operand) -> Result<BoolExpr, String> {
let op = match op {
"=" => CompOp::Eq,
"<>" | "!=" => CompOp::Ne,
other => {
t.note_operand_refusal(bool_operand_refusal(other));
return self.condition(true);
}
};
if let Operand::Value(PartiqlValue::Literal(AttributeValue::BOOL(b))) = value {
return self.condition(b == (op == CompOp::Eq));
}
Ok(BoolExpr::Leaf(WhereCondition::Compare(
self.operand(),
op,
value,
)))
}
fn listed(&self, members: Vec<Operand>) -> BoolExpr {
BoolExpr::Leaf(WhereCondition::CompareIn(self.operand(), members))
}
}
fn bool_operand_refusal(op: &str) -> String {
format!(
"Incorrect operand type for operator or function; operator or function: {op}, \
operand type: BOOL"
)
}
fn condition_function_name(tok: &str) -> Option<&'static str> {
["attribute_exists", "attribute_not_exists"]
.into_iter()
.find(|name| is_keyword(tok, name))
}
fn is_condition_function(tok: &str) -> bool {
condition_function_name(tok).is_some() || is_keyword(tok, "attribute_type")
}
fn starts_value(t: &Tokenizer, tok: &str) -> Result<bool, String> {
Ok(tok == "?"
|| tok.starts_with(['\'', '`', '[', '{', '<'])
|| is_number_token(tok)
|| ((tok == "-" || tok == "+") && t.peek_token_at(1)?.is_some_and(|n| is_number_token(&n)))
|| is_any(tok, &["TRUE", "FALSE", "NULL", "MISSING"]))
}
fn parse_condition(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
let Some(tok) = t.peek_token()? else {
return single_condition(t, depth);
};
let calls = |offset: usize, t: &Tokenizer| -> Result<bool, String> {
Ok(t.peek_token_at(offset + 1)?.as_deref() == Some("("))
};
if is_any(&tok, &["TRUE", "FALSE"])
&& t.peek_token_at(1)?
.is_some_and(|op| is_comparison_operator(&op))
&& t.peek_token_at(2)?
.is_some_and(|f| is_condition_function(&f))
&& calls(2, t)?
{
let left = parse_value(t, depth)?;
let op = t.next_token()?.unwrap_or_default();
return match parse_condition_function(t, depth)? {
Some(function) => function.compared(t, &op, Operand::Value(left)),
None => Ok(refused_condition()),
};
}
if is_name(&tok) && !calls(0, t)? {
let end = path_end(t)?;
if t.peek_token_at(end)?
.is_some_and(|op| is_comparison_operator(&op))
&& t.peek_token_at(end + 1)?
.is_some_and(|f| is_condition_function(&f))
&& calls(end + 1, t)?
{
let first = t.next_token()?.unwrap_or_default();
let path = parse_dotted_path_from_token(&first, t)?;
let op = t.next_token()?.unwrap_or_default();
return match parse_condition_function(t, depth)? {
Some(function) => function.compared(t, &op, Operand::Path(path)),
None => Ok(refused_condition()),
};
}
}
let bare_word = tok.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_');
if bare_word && calls(0, t)? {
if is_keyword(&tok, "EXISTS")
&& let Some(select) = subquery_select(t)
{
let at = t.position(t.span_at(select));
t.note_term(format!("Unsupported token in expression: SELECT at {at}"));
skip_call(t, depth)?;
return Ok(refused_condition());
}
if is_condition_function(&tok) {
let Some(function) = parse_condition_function(t, depth)? else {
skip_comparison(t, depth)?;
return Ok(refused_condition());
};
return condition_function_tail(t, depth, function);
}
if is_keyword(&tok, "size") {
let left = parse_operand(t, depth)?;
return finish_comparison(t, depth, left);
}
if !is_any(&tok, WHERE_FUNCTIONS) {
t.note_unrecognized_function(t.pos);
skip_call(t, depth)?;
return Ok(refused_condition());
}
}
if starts_value(t, &tok)? {
let left = parse_operand(t, depth)?;
return finish_comparison(t, depth, left);
}
single_condition(t, depth)
}
fn single_condition(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
Ok(match parse_single_condition(t, depth)? {
WhereCondition::Not(inner) => BoolExpr::Not(Box::new(BoolExpr::Leaf(*inner))),
cond => BoolExpr::Leaf(cond),
})
}
fn refused_condition() -> BoolExpr {
BoolExpr::Leaf(WhereCondition::Exists(String::new()))
}
fn condition_function_tail(
t: &mut Tokenizer,
depth: usize,
function: ConditionFunction,
) -> Result<BoolExpr, String> {
let Some(next) = t.peek_token()? else {
return function.condition(true);
};
if is_comparison_operator(&next) {
t.next_token()?;
let right = parse_comparand(t, depth)?;
return function.compared(t, &next, right);
}
let negated = is_keyword(&next, "NOT")
&& t.peek_token_at(1)?
.is_some_and(|op| is_any(&op, &["BETWEEN", "IN"]));
let next = if negated {
t.next_token()?;
t.peek_token()?.unwrap_or_default()
} else {
next
};
if is_keyword(&next, "BETWEEN") {
t.note_operand_refusal(bool_operand_refusal("BETWEEN"));
t.next_token()?;
parse_bound(t, depth)?;
expect_between_and(t)?;
parse_bound(t, depth)?;
return Ok(refused_condition());
}
if is_keyword(&next, "IN") {
t.next_token()?;
let members = parse_in_list(t, depth, parse_comparand)?;
note_in_operand_count(t, members.len());
let listed = function.listed(members);
return Ok(if negated {
BoolExpr::Not(Box::new(listed))
} else {
listed
});
}
function.condition(true)
}
fn parse_condition_function(
t: &mut Tokenizer,
depth: usize,
) -> Result<Option<ConditionFunction>, String> {
if t.peek_token()?
.is_some_and(|tok| is_keyword(&tok, "attribute_type"))
{
return Ok(
parse_attribute_type(t, depth)?.map(|(operand, kind)| ConditionFunction {
holds: WhereCondition::AttributeType(operand, kind),
}),
);
}
let name_tok = t.next_token()?.unwrap_or_default();
let name = condition_function_name(&name_tok).unwrap_or("attribute_exists");
expect_char(t, "(")?;
let mut operands: Vec<Option<String>> = Vec::new();
if t.peek_token()?.as_deref() == Some(")") {
t.next_token()?;
} else {
loop {
let tok = t.peek_token()?.ok_or(UNEXPECTED_TERM)?;
if is_name(&tok) {
t.next_token()?;
operands.push(Some(parse_dotted_path_from_token(&tok, t)?));
} else {
parse_value(t, depth)?;
operands.push(None);
}
match t.next_token()?.as_deref() {
Some(",") => continue,
Some(")") => break,
Some(other) => return Err(format!("Expected ')' but got '{other}'")),
None => return Err("Expected ')'".to_string()),
}
}
}
let path = match operands.as_slice() {
[Some(path)] => path.clone(),
[None] => {
t.note_operand_refusal(format!(
"Operator or function requires a document path; operator or function: {name}"
));
String::new()
}
_ => {
t.note_operand_refusal(operand_count_refusal(name, operands.len()));
String::new()
}
};
Ok(Some(ConditionFunction {
holds: if name == "attribute_exists" {
WhereCondition::Exists(path)
} else {
WhereCondition::NotExists(path)
},
}))
}
fn operand_count_refusal(name: &str, count: usize) -> String {
format!(
"Incorrect number of operands for operator or function; operator or function: \
{name}, number of operands: {count}"
)
}
fn call_operands(t: &mut Tokenizer, depth: usize) -> Result<Vec<Operand>, String> {
t.next_token()?;
expect_char(t, "(")?;
let mut operands = Vec::new();
if t.peek_token()?.as_deref() == Some(")") {
t.next_token()?;
return Ok(operands);
}
loop {
operands.push(parse_operand(t, depth)?);
match t.next_token()?.as_deref() {
Some(",") => continue,
Some(")") => return Ok(operands),
Some(other) => return Err(format!("Expected ')' but got '{other}'")),
None => return Err("Expected ')'".to_string()),
}
}
}
fn parse_attribute_type(
t: &mut Tokenizer,
depth: usize,
) -> Result<Option<(Operand, PartiqlValue)>, String> {
let mut operands = call_operands(t, depth + 1)?;
if operands.len() != 2 {
t.note_operand_refusal(operand_count_refusal("attribute_type", operands.len()));
return Ok(None);
}
let kind = operands
.pop()
.unwrap_or(Operand::Value(PartiqlValue::Parameter(0)));
let operand = operands
.pop()
.unwrap_or(Operand::Value(PartiqlValue::Parameter(0)));
let operand_type = |kind: &str| {
format!(
"Incorrect operand type for operator or function; operator or function: \
attribute_type, operand type: {kind}"
)
};
let refusal = match &kind {
Operand::Value(PartiqlValue::Literal(AttributeValue::S(name))) => {
attribute_type_name_refusal(name)
}
Operand::Value(PartiqlValue::Literal(other)) => Some(operand_type(other.type_name())),
Operand::Value(PartiqlValue::Parameter(_)) => None,
Operand::Path(path) if operand == Operand::Path(path.clone()) => {
Some(not_distinct("attribute_type", path))
}
Operand::Path(_) => Some(operand_type(ATTRIBUTE_TYPE_OPERAND_TYPES)),
Operand::Size(_) => Some(operand_type("N")),
Operand::Condition(_) => Some(operand_type("BOOL")),
};
if let Some(message) = refusal {
t.note_operand_refusal(message);
return Ok(None);
}
let Operand::Value(kind) = kind else {
return Ok(None);
};
Ok(Some((operand, kind)))
}
pub(crate) fn attribute_type_name_refusal(name: &str) -> Option<String> {
(!ATTRIBUTE_TYPES.contains(&name)).then(|| {
format!(
"Invalid attribute type name found; type: {name}, valid types: \
{{N,BS,L,B,NULL,M,S,SS,NS,BOOL}}"
)
})
}
fn parse_operand(t: &mut Tokenizer, depth: usize) -> Result<Operand, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
let tok = match t.peek_token()? {
None => return Err(MISSING_RIGHT_HAND_SIDE.to_string()),
Some(tok) if tok == ";" => return Err(MISSING_RIGHT_HAND_SIDE.to_string()),
Some(tok) => tok,
};
if is_keyword(&tok, "MISSING") {
t.next_token()?;
t.note_term(MISSING_COMPARED.to_string());
return Ok(Operand::Value(PartiqlValue::Literal(AttributeValue::NULL(
true,
))));
}
if is_keyword(&tok, "size") && t.peek_token_at(1)?.as_deref() == Some("(") {
let mut operands = call_operands(t, depth + 1)?;
if operands.len() != 1 {
t.note_operand_refusal(operand_count_refusal("size", operands.len()));
return Ok(Operand::Value(PartiqlValue::Literal(AttributeValue::NULL(
true,
))));
}
let inner = operands
.pop()
.unwrap_or(Operand::Value(PartiqlValue::Parameter(0)));
return Ok(Operand::Size(Box::new(inner)));
}
if is_name(&tok) {
t.next_token()?;
return Ok(Operand::Path(parse_dotted_path_from_token(&tok, t)?));
}
Ok(Operand::Value(parse_value(t, depth)?))
}
fn parse_bound(t: &mut Tokenizer, depth: usize) -> Result<PartiqlValue, String> {
if t.peek_token()?
.is_some_and(|tok| is_keyword(&tok, "MISSING"))
{
t.next_token()?;
t.note_term(MISSING_COMPARED.to_string());
return Ok(PartiqlValue::Literal(AttributeValue::NULL(true)));
}
parse_value(t, depth)
}
fn expect_between_and(t: &mut Tokenizer) -> Result<(), String> {
if !t.next_token()?.is_some_and(|tok| is_keyword(&tok, "AND")) {
return Err("Expected AND keyword".to_string());
}
match t.peek_token()?.as_deref() {
None | Some(";") => Err("Expected expression after AND".to_string()),
Some(_) => Ok(()),
}
}
fn parse_in_list<T>(
t: &mut Tokenizer,
depth: usize,
member: fn(&mut Tokenizer, usize) -> Result<T, String>,
) -> Result<Vec<T>, String> {
let open = t.next_token()?.ok_or("Expected '(' or '[' after IN")?;
let close = match open.as_str() {
"(" => ")",
"[" => "]",
other => return Err(format!("Expected '(' or '[' after IN, got '{other}'")),
};
let mut values = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of IN list")?;
if peek == close {
t.next_token()?;
return Ok(values);
}
if peek == "," {
t.next_token()?;
continue;
}
values.push(member(t, depth)?);
}
}
fn note_in_operand_count(t: &mut Tokenizer, count: usize) {
if count > MAX_IN_OPERANDS {
t.note_operand_refusal(format!(
"The IN operator is provided with too many operands; number of operands: {count}"
));
}
}
fn parse_bound_operand(t: &mut Tokenizer, depth: usize) -> Result<Operand, String> {
parse_operand(t, depth)
}
fn parse_comparand(t: &mut Tokenizer, depth: usize) -> Result<Operand, String> {
let calls = t.peek_token_at(1)?.as_deref() == Some("(");
if calls
&& t.peek_token()?
.is_some_and(|tok| is_condition_function(&tok))
{
return Ok(match parse_condition_function(t, depth)? {
Some(function) => function.operand(),
None => Operand::Value(PartiqlValue::Literal(AttributeValue::NULL(true))),
});
}
parse_operand(t, depth)
}
fn all_values(operands: &[Operand]) -> Option<Vec<PartiqlValue>> {
operands
.iter()
.map(|operand| match operand {
Operand::Value(value) => Some(value.clone()),
_ => None,
})
.collect()
}
fn finish_comparison(t: &mut Tokenizer, depth: usize, left: Operand) -> Result<BoolExpr, String> {
if matches!(left, Operand::Size(_)) {
while let Some(op) = t.peek_token()?.filter(|op| op == "+" || op == "-") {
t.note_operand_refusal(format!(
"Unsupported operator in Condition Expression. Operator: {op}"
));
t.next_token()?;
parse_operand(t, depth)?;
}
}
let next = t.peek_token()?;
match next.as_deref() {
Some(tok)
if is_keyword(tok, "NOT")
&& t.peek_token_at(1)?
.is_some_and(|op| is_any(&op, &["BETWEEN", "IN"])) =>
{
t.next_token()?; Ok(BoolExpr::Not(Box::new(finish_comparison(t, depth, left)?)))
}
Some(op) if is_comparison_operator(op) => {
let op_tok = t.next_token()?.unwrap_or_default();
let op = comparison_operator(&op_tok)?;
match parse_comparand(t, depth)? {
Operand::Condition(holds) => {
ConditionFunction { holds: *holds }.compared(t, &op_tok, left)
}
right => Ok(BoolExpr::Leaf(comparison(t, left, op, &op_tok, right))),
}
}
Some(tok) if is_keyword(tok, "BETWEEN") => {
t.next_token()?;
let low = parse_operand(t, depth)?;
expect_between_and(t)?;
let high = parse_operand(t, depth)?;
Ok(BoolExpr::Leaf(WhereCondition::CompareBetween(
left, low, high,
)))
}
Some(tok) if is_keyword(tok, "IN") => {
t.next_token()?;
let members = parse_in_list(t, depth, parse_comparand)?;
note_in_operand_count(t, members.len());
Ok(BoolExpr::Leaf(WhereCondition::CompareIn(left, members)))
}
Some(tok) if is_keyword(tok, "IS") && matches!(left, Operand::Size(_)) => {
t.note_operand_refusal(
"The left hand side argument to IS must be a Variable Reference, Literal, or \
Path"
.to_string(),
);
t.next_token()?;
if t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "NOT")) {
t.next_token()?;
}
read_type_name(t)?;
Ok(refused_condition())
}
_ if matches!(left, Operand::Size(_)) => Ok(BoolExpr::NonBoolean("N")),
_ => Err("Expected operator after attribute path".to_string()),
}
}
fn comparison(
t: &mut Tokenizer,
left: Operand,
op: CompOp,
op_tok: &str,
right: Operand,
) -> WhereCondition {
match (left, right) {
(Operand::Path(path), Operand::Value(value)) => {
WhereCondition::Comparison(Condition { path, op, value })
}
(Operand::Value(value), Operand::Path(path)) => WhereCondition::Comparison(Condition {
path,
op: mirrored(op),
value,
}),
(Operand::Path(a), Operand::Path(b)) if a == b => {
t.note_operand_refusal(not_distinct(op_tok, &a));
WhereCondition::Exists(String::new())
}
(left, right) => WhereCondition::Compare(left, op, right),
}
}
fn subquery_select(t: &Tokenizer) -> Option<usize> {
let mut open = 0usize;
for i in t.pos + 1..t.tokens.len() {
match t.tokens[i].as_str() {
"(" => open += 1,
")" => {
open -= 1;
if open == 0 {
return None;
}
}
tok if is_keyword(tok, "SELECT") => return Some(i),
_ => {}
}
}
None
}
fn skip_call(t: &mut Tokenizer, depth: usize) -> Result<(), String> {
t.next_token()?;
let mut open = 0usize;
loop {
match t.next_token()?.as_deref() {
Some("(") => open += 1,
Some(")") if open == 1 => break,
Some(")") => open -= 1,
Some(_) => {}
None => return Err("Expected ')'".to_string()),
}
}
skip_comparison(t, depth)
}
fn skip_comparison(t: &mut Tokenizer, depth: usize) -> Result<(), String> {
if let Some(op) = t.peek_token()?
&& is_comparison_operator(&op)
{
t.next_token()?;
parse_operand(t, depth)?;
}
Ok(())
}
fn push_not_down(expr: BoolExpr, negated: bool) -> Result<BoolExpr, String> {
match expr {
BoolExpr::Not(inner) => push_not_down(*inner, !negated),
BoolExpr::And(terms) => {
let terms = terms
.into_iter()
.map(|e| push_not_down(e, negated))
.collect::<Result<Vec<_>, _>>()?;
Ok(if negated {
BoolExpr::Or(terms)
} else {
BoolExpr::And(terms)
})
}
BoolExpr::Or(terms) => {
let terms = terms
.into_iter()
.map(|e| push_not_down(e, negated))
.collect::<Result<Vec<_>, _>>()?;
Ok(if negated {
BoolExpr::And(terms)
} else {
BoolExpr::Or(terms)
})
}
BoolExpr::Leaf(cond) => {
if negated {
negate_condition(cond)
} else {
Ok(BoolExpr::Leaf(cond))
}
}
BoolExpr::NonBoolean(_) => Ok(refused_condition()),
}
}
fn negate_condition(cond: WhereCondition) -> Result<BoolExpr, String> {
use WhereCondition as W;
let not_cmp = |path: String, op: CompOp, value: PartiqlValue| -> BoolExpr {
W::NotComparison(Condition { path, op, value }).into()
};
Ok(match cond {
W::Comparison(c) => W::NotComparison(c).into(),
W::NotComparison(c) => W::Comparison(c).into(),
W::Exists(p) => W::NotExists(p).into(),
W::NotExists(p) => W::Exists(p).into(),
W::BeginsWith(p, v) => W::NotBeginsWith(p, v).into(),
W::NotBeginsWith(p, v) => W::BeginsWith(p, v).into(),
W::IsMissing(p) => W::IsNotMissing(p).into(),
W::IsNotMissing(p) => W::IsMissing(p).into(),
W::IsType(p, test) => W::IsNotType(p, test).into(),
W::IsNotType(p, test) => W::IsType(p, test).into(),
W::Contains(p, v) => W::NotContains(p, v).into(),
W::NotContains(p, v) => W::Contains(p, v).into(),
W::Between(p, low, high) => W::NotBetween(p, low, high).into(),
W::NotBetween(p, low, high) => W::Between(p, low, high).into(),
W::In(p, values) if p.len().saturating_mul(values.len()) > MAX_CLAUSE_BYTES => {
return Err(TOO_COMPLEX.to_string());
}
W::In(p, values) => BoolExpr::And(
values
.into_iter()
.map(|v| not_cmp(p.clone(), CompOp::Eq, v))
.collect(),
),
W::Not(inner) => (*inner).into(),
other @ (W::Compare(..)
| W::CompareBetween(..)
| W::CompareIn(..)
| W::AttributeType(..)) => W::Not(Box::new(other)).into(),
})
}
impl From<WhereCondition> for BoolExpr {
fn from(cond: WhereCondition) -> Self {
BoolExpr::Leaf(cond)
}
}
const MAX_OR_GROUPS: usize = 256;
const MAX_EXPR_DEPTH: usize = 64;
const MAX_TOTAL_CONDITIONS: usize = 4096;
const MAX_OR_SLOTS: usize = 4 * MAX_TOTAL_CONDITIONS;
const TOO_COMPLEX: &str = "WHERE clause is too complex to evaluate";
const MAX_CLAUSE_BYTES: usize = 4 * 1024 * 1024;
const MAX_IN_OPERANDS: usize = 100;
fn condition_bytes(condition: &WhereCondition) -> usize {
fn value(v: &PartiqlValue) -> usize {
match v {
PartiqlValue::Literal(av) => av.size(),
PartiqlValue::Parameter(_) => 0,
}
}
fn operand(o: &Operand) -> usize {
match o {
Operand::Path(p) => p.len(),
Operand::Value(v) => value(v),
Operand::Size(inner) => operand(inner),
Operand::Condition(inner) => condition_bytes(inner),
}
}
use WhereCondition as W;
match condition {
W::Comparison(c) | W::NotComparison(c) => c.path.len() + value(&c.value),
W::Exists(p) | W::NotExists(p) | W::IsMissing(p) | W::IsNotMissing(p) => p.len(),
W::IsType(p, _) | W::IsNotType(p, _) => p.len(),
W::BeginsWith(p, v) | W::NotBeginsWith(p, v) | W::Contains(p, v) | W::NotContains(p, v) => {
p.len() + value(v)
}
W::Between(p, low, high) | W::NotBetween(p, low, high) => {
p.len() + value(low) + value(high)
}
W::In(p, values) => p.len() + values.iter().map(value).sum::<usize>(),
W::Compare(left, _, right) => operand(left) + operand(right),
W::CompareBetween(o, low, high) => operand(o) + operand(low) + operand(high),
W::CompareIn(o, values) => operand(o) + values.iter().map(operand).sum::<usize>(),
W::AttributeType(o, v) => operand(o) + value(v),
W::Not(inner) => condition_bytes(inner),
}
}
const TOO_DEEP: &str = "Statement is nested too deeply to parse";
const MAX_OPERATORS: usize = 300;
type Groups = Vec<Vec<Arc<WhereCondition>>>;
fn slots(groups: &Groups) -> usize {
groups.iter().map(Vec::len).sum()
}
fn to_dnf(tree: &ConditionTree) -> Result<Groups, String> {
let too_complex = || Err(TOO_COMPLEX.to_string());
match tree {
ConditionTree::Leaf(cond) => Ok(vec![vec![Arc::clone(cond)]]),
ConditionTree::Or(terms) => {
let mut out: Groups = Vec::new();
let mut held = 0usize;
for term in terms {
let term_groups = to_dnf(term)?;
let term_slots = slots(&term_groups);
if out.len().saturating_add(term_groups.len()) > MAX_OR_GROUPS
|| held.saturating_add(term_slots) > MAX_OR_SLOTS
{
return too_complex();
}
held += term_slots;
out.extend(term_groups);
}
Ok(out)
}
ConditionTree::And(terms) => {
let mut out: Groups = vec![Vec::new()];
let mut held = 0usize;
for term in terms {
let term_groups = to_dnf(term)?;
let term_slots = slots(&term_groups);
let next_groups = out.len().saturating_mul(term_groups.len());
let next_slots = held
.saturating_mul(term_groups.len())
.saturating_add(term_slots.saturating_mul(out.len()));
if next_groups > MAX_OR_GROUPS || next_slots > MAX_TOTAL_CONDITIONS {
return too_complex();
}
if let [only] = term_groups.as_slice() {
for group in out.iter_mut() {
group.extend(only.iter().cloned());
}
} else {
let mut next = Vec::with_capacity(next_groups);
for existing in &out {
for group in &term_groups {
let mut combined = Vec::with_capacity(existing.len() + group.len());
combined.extend(existing.iter().cloned());
combined.extend(group.iter().cloned());
next.push(combined);
}
}
out = next;
}
held = next_slots;
}
Ok(out)
}
}
}
fn parse_single_condition(t: &mut Tokenizer, depth: usize) -> Result<WhereCondition, String> {
let path_start = t.pos;
let tok = match t.next_token()? {
None => return Err(UNEXPECTED_TERM.to_string()),
Some(tok) if tok == ";" => return Err(UNEXPECTED_TERM.to_string()),
Some(tok) => tok,
};
if !is_keyword(&tok, "NOT") && is_any(&tok, OPERATOR_WORDS) {
return Err(UNEXPECTED_OPERATOR.to_string());
}
if is_any(&tok, CLAUSE_KEYWORDS) {
return Err(UNEXPECTED_KEYWORD.to_string());
}
let tok_upper = tok.to_uppercase();
match tok_upper.as_str() {
"EXISTS" => {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
expect_char(t, ")")?;
Ok(WhereCondition::Exists(path))
}
"BEGINS_WITH" => {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
let comma = t.next_token()?.ok_or("Expected ',' in BEGINS_WITH")?;
if comma != "," {
return Err(format!("Expected ',' but got '{comma}'"));
}
let value = parse_value(t, depth)?;
expect_char(t, ")")?;
Ok(WhereCondition::BeginsWith(path, value))
}
"CONTAINS" => {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
let comma = t.next_token()?.ok_or("Expected ',' in CONTAINS")?;
if comma != "," {
return Err(format!("Expected ',' but got '{comma}'"));
}
let value = parse_value(t, depth)?;
expect_char(t, ")")?;
Ok(WhereCondition::Contains(path, value))
}
"NOT" => {
let func = t.next_token()?.ok_or("Expected function name after NOT")?;
if func.eq_ignore_ascii_case("BEGINS_WITH") {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
let comma = t.next_token()?.ok_or("Expected ',' in NOT BEGINS_WITH")?;
if comma != "," {
return Err(format!("Expected ',' but got '{comma}'"));
}
let value = parse_value(t, depth)?;
expect_char(t, ")")?;
Ok(WhereCondition::NotBeginsWith(path, value))
} else {
Err(format!("Unsupported NOT function: {func}"))
}
}
_ => {
let path = parse_dotted_path_from_token(&tok, t)?;
let next = t
.peek_token()?
.ok_or("Expected operator after attribute path")?;
let next_upper = next.to_uppercase();
match next_upper.as_str() {
"BETWEEN" => {
t.next_token()?; path_between(t, depth, path)
}
"IN" => {
t.next_token()?; let members = parse_in_list(t, depth, parse_comparand)?;
Ok(path_in(t, path, members))
}
"NOT" => {
let after = t.peek_token_at(1)?.unwrap_or_default();
if is_keyword(&after, "IN") {
t.next_token()?; t.next_token()?; let members = parse_in_list(t, depth, parse_comparand)?;
Ok(WhereCondition::Not(Box::new(path_in(t, path, members))))
} else if is_keyword(&after, "BETWEEN") {
t.next_token()?; t.next_token()?; Ok(WhereCondition::Not(Box::new(path_between(t, depth, path)?)))
} else if is_keyword(&after, "LIKE") {
Err("Unknown operator: NOT".to_string())
} else {
Err("Unexpected token after expression".to_string())
}
}
"IS" => {
t.next_token()?; let negated = t.peek_token()?.is_some_and(|tok| is_keyword(&tok, "NOT"));
if negated {
t.next_token()?;
}
match read_type_name(t)? {
None => Err("Expected type name".to_string()),
Some(TypeName::Missing) if negated => {
Ok(WhereCondition::IsNotMissing(path))
}
Some(TypeName::Missing) => Ok(WhereCondition::IsMissing(path)),
Some(TypeName::Test(test)) if negated => {
Ok(WhereCondition::IsNotType(path, test))
}
Some(TypeName::Test(test)) => Ok(WhereCondition::IsType(path, test)),
Some(TypeName::Unsupported(name)) => {
let at = t.position(t.span_at(path_start));
t.note_term(format!(
"Unsupported data type for IS operator: {name}. Supported \
data types are [MISSING, NULL, BOOLEAN, BLOB, STRING, \
NUMERIC, STRUCT, TUPLE, LIST] at {at}"
));
Ok(WhereCondition::IsMissing(path))
}
}
}
_ => {
let op_tok = t.next_token()?.ok_or("Expected comparison operator")?;
let op = comparison_operator(&op_tok)?;
let right = parse_operand(t, depth)?;
Ok(comparison(t, Operand::Path(path), op, &op_tok, right))
}
}
}
}
}
fn path_between(t: &mut Tokenizer, depth: usize, path: String) -> Result<WhereCondition, String> {
let low = parse_bound_operand(t, depth)?;
expect_between_and(t)?;
let high = parse_bound_operand(t, depth)?;
if names_path(&[&low, &high], &path) {
t.note_operand_refusal(not_distinct("BETWEEN", &path));
return Ok(WhereCondition::Exists(String::new()));
}
Ok(match (low, high) {
(Operand::Value(low), Operand::Value(high)) => WhereCondition::Between(path, low, high),
(low, high) => WhereCondition::CompareBetween(Operand::Path(path), low, high),
})
}
fn path_in(t: &mut Tokenizer, path: String, members: Vec<Operand>) -> WhereCondition {
if names_path(&members.iter().collect::<Vec<_>>(), &path) {
t.note_operand_refusal(not_distinct("IN", &path));
return WhereCondition::Exists(String::new());
}
note_in_operand_count(t, members.len());
match all_values(&members) {
Some(values) => WhereCondition::In(path, values),
None => WhereCondition::CompareIn(Operand::Path(path), members),
}
}
fn names_path(operands: &[&Operand], path: &str) -> bool {
operands
.iter()
.any(|operand| matches!(operand, Operand::Path(other) if other == path))
}
fn not_distinct(operator: &str, path: &str) -> String {
format!(
"The first operand must be distinct from the remaining operands for this operator or \
function; operator: {operator}, first operand: [{}]",
path_parts(path)
)
}
fn path_parts(path: &str) -> String {
match super::split_path_segments(path) {
Some(segments) => segments
.iter()
.map(|segment| match segment {
super::PathSegment::Key(key) => key.to_string(),
super::PathSegment::Index(index) => format!("[{index}]"),
})
.collect::<Vec<_>>()
.join(", "),
None => path.to_string(),
}
}
fn path_end(t: &Tokenizer) -> Result<usize, String> {
let mut end = 1;
loop {
match t.peek_token_at(end)?.as_deref() {
Some(".") => end += 2,
Some("[") => end += 3,
_ => return Ok(end),
}
}
}
fn parse_dotted_path_from_token(first_tok: &str, t: &mut Tokenizer) -> Result<String, String> {
let mut path = path_segment(first_tok);
while let Some(ref next) = t.peek_token()? {
if next == "." {
t.next_token()?; let seg = t.next_token()?.ok_or("Expected attribute name after '.'")?;
check_dot_component(&seg)?;
path.push('.');
path.push_str(&path_segment(&seg));
} else if next == "[" {
t.next_token()?; let idx = t.next_token()?.ok_or("Expected index in '[]'")?;
let close = t.next_token()?.ok_or("Expected ']'")?;
if close != "]" {
return Err(format!("Expected ']' but got '{close}'"));
}
path.push('[');
path.push_str(&idx);
path.push(']');
} else {
break;
}
}
Ok(path)
}
fn path_segment(tok: &str) -> String {
let name = unquote(tok);
if tok.starts_with('"') {
super::attribute_path(&name).into_owned()
} else {
name
}
}
fn parse_function_path(t: &mut Tokenizer) -> Result<String, String> {
let tok = t.next_token()?.ok_or("Expected path in function")?;
parse_dotted_path_from_token(&tok, t)
}
fn expect_char(t: &mut Tokenizer, expected: &str) -> Result<(), String> {
let tok = t.next_token()?.ok_or(format!("Expected '{expected}'"))?;
if tok != expected {
return Err(format!("Expected '{expected}' but got '{tok}'"));
}
Ok(())
}
fn parse_value(t: &mut Tokenizer, depth: usize) -> Result<PartiqlValue, String> {
t.reading_literal += 1;
let value = read_value(t, depth);
t.reading_literal -= 1;
value
}
fn read_value(t: &mut Tokenizer, depth: usize) -> Result<PartiqlValue, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
t.note_depth();
let tok = t.next_token()?.ok_or("Expected value")?;
if tok == "?" {
let idx = t.next_param_index();
return Ok(PartiqlValue::Parameter(idx));
}
if !is_keyword(&tok, "NOT") && is_any(&tok, OPERATOR_WORDS) {
return Err(UNEXPECTED_OPERATOR.to_string());
}
if is_any(&tok, CLAUSE_KEYWORDS) {
return Err(UNEXPECTED_KEYWORD.to_string());
}
if tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2 {
let s = tok[1..tok.len() - 1].to_string();
return Ok(PartiqlValue::Literal(AttributeValue::S(s)));
}
if tok == "<"
&& t.peek_token()?.as_deref() == Some("<>")
&& t.peek_token_at(1)?.as_deref() == Some(">")
{
t.next_token()?;
t.next_token()?;
t.note_empty_bag();
return Ok(PartiqlValue::Literal(AttributeValue::SS(Vec::new())));
}
if tok == "<" && t.peek_token()?.as_deref() == Some("<") {
t.next_token()?; return Ok(PartiqlValue::Literal(parse_bag(t, depth)?));
}
if tok.starts_with('`') {
return Ok(PartiqlValue::Literal(ion_literal(t, &tok)?));
}
if tok == "[" {
let mut items = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of list")?;
if peek == "]" {
t.next_token()?;
break;
}
if peek == "," {
t.next_token()?;
continue;
}
t.value_path.push(format!("[{}]", items.len()));
let item = parse_value(t, depth + 1);
t.value_path.pop();
items.push(item?);
expect_separator(t, "]", "Expected RIGHT_BRACKET")?;
}
let mut avs = Vec::new();
for item in items {
match item {
PartiqlValue::Literal(av) => avs.push(av),
PartiqlValue::Parameter(_) => {
return Err(
"Parameter placeholders inside list literals are not yet supported"
.to_string(),
);
}
}
}
return Ok(PartiqlValue::Literal(AttributeValue::L(avs)));
}
if tok == "{" {
let mut map = HashMap::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of map literal")?;
if peek == "}" {
t.next_token()?;
break;
}
if peek == "," {
t.next_token()?;
continue;
}
let key_tok = t.next_token()?.ok_or("Expected key in map literal")?;
let key = unquote(&key_tok);
t.note_key(&key);
let colon = t.next_token()?.ok_or("Expected ':'")?;
if colon != ":" {
return Err(format!("Expected ':' but got '{colon}'"));
}
t.value_path.push(format!(".{key}"));
let val = parse_value(t, depth + 1);
t.value_path.pop();
match val? {
PartiqlValue::Literal(av) => {
map.insert(key, av);
}
PartiqlValue::Parameter(_) => {
return Err(
"Parameter placeholders inside map literals are not yet supported"
.to_string(),
);
}
}
expect_separator(t, "}", "Expected RIGHT_CURLY")?;
}
return Ok(PartiqlValue::Literal(AttributeValue::M(map)));
}
if (tok == "-" || tok == "+")
&& let Some(ref next) = t.peek_token()?
&& is_number_token(next)
{
let num = t.next_token()?.unwrap();
let text = format!("{tok}{num}");
t.note_number(&text);
return Ok(PartiqlValue::Literal(AttributeValue::N(text)));
}
if is_number_token(&tok) {
t.note_number(&tok);
return Ok(PartiqlValue::Literal(AttributeValue::N(tok)));
}
match tok.to_uppercase().as_str() {
"TRUE" => return Ok(PartiqlValue::Literal(AttributeValue::BOOL(true))),
"FALSE" => return Ok(PartiqlValue::Literal(AttributeValue::BOOL(false))),
"NULL" => return Ok(PartiqlValue::Literal(AttributeValue::NULL(true))),
_ => {}
}
if tok
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
{
let mut path = tok.clone();
while let Some(ref next) = t.peek_token()? {
if next == "." {
t.next_token()?;
let seg = t.next_token()?.ok_or("Expected attribute name after '.'")?;
check_dot_component(&seg)?;
path.push('.');
path.push_str(&path_segment(&seg));
} else {
break;
}
}
return Ok(PartiqlValue::Literal(AttributeValue::S(path)));
}
if matches!(tok.as_str(), "." | "-" | "+") {
return Err("Unexpected term".to_string());
}
Err(format!("Unexpected value token: {tok}"))
}
fn check_literal_number(n: &str) -> Result<(), String> {
if let Some((precision, scale)) = literal_precision_and_scale(n)
&& precision - scale >= i64::from(i32::MAX)
{
return Err(
"Number underflow. Attempting to store a number with magnitude smaller \
than supported range"
.to_string(),
);
}
crate::types::validate_dynamo_number(n).map_err(|e| match e {
crate::errors::DynoxideError::ValidationException(message) => message,
other => other.to_string(),
})
}
fn literal_precision_and_scale(n: &str) -> Option<(i64, i64)> {
let unsigned = n.trim_start_matches(['+', '-']);
let (coefficient, exponent) = match unsigned.find(['e', 'E']) {
Some(at) => (&unsigned[..at], unsigned[at + 1..].parse::<i64>().ok()?),
None => (unsigned, 0),
};
let fraction_digits = coefficient
.split_once('.')
.map_or(0, |(_, fraction)| fraction.len());
let digits: String = coefficient.chars().filter(char::is_ascii_digit).collect();
let significant = digits.trim_start_matches('0').len().max(1);
let scale = i64::try_from(fraction_digits).ok()? - exponent;
Some((i64::try_from(significant).ok()?, scale))
}
fn is_number_token(tok: &str) -> bool {
let mut chars = tok.chars();
match chars.next() {
Some(c) if c.is_ascii_digit() => true,
Some('.') => chars.next().is_some_and(|c| c.is_ascii_digit()),
_ => false,
}
}
enum BagMember {
Str(String),
Num(String),
Null,
Parameter,
Ion(&'static str),
Other,
}
const BAG_REFUSAL: &str =
"Unsupported data type in Bag. DynamoDB only supports either numbers or strings in bags";
fn parse_bag(t: &mut Tokenizer, depth: usize) -> Result<AttributeValue, String> {
let mut members: Vec<(BagMember, Span)> = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of set literal")?;
if peek == ">" {
t.next_token()?;
if t.peek_token()?.as_deref() == Some(">") {
t.next_token()?;
}
break;
}
if peek == "," {
t.next_token()?;
continue;
}
let start = t.pos;
let member = read_bag_member(t, depth)?;
let span = member_span(t, start, &member);
members.push((member, span));
expect_separator(t, ">", "Expected RIGHT_DOUBLE_ANGLE_BRACKET")?;
}
let Some((first, first_span)) = members.first() else {
t.note_empty_bag();
return Ok(AttributeValue::SS(Vec::new()));
};
let strings = matches!(first, BagMember::Str(_));
let path: String = t.value_path.concat();
let own_refusal = |member: &BagMember, span: &Span| match member {
BagMember::Parameter => Some(format!(
"Unsupported data type: Parameter under key root{path} at {}",
t.position(*span)
)),
BagMember::Ion(kind) => Some(format!(
"Ion Literals are not supported: {kind} under key root{path} at {}",
t.position(*span)
)),
_ => None,
};
let refusal = match first {
BagMember::Str(_) => members[1..]
.iter()
.find(|(m, _)| !matches!(m, BagMember::Str(_)))
.map(|(m, span)| {
own_refusal(m, span)
.unwrap_or_else(|| format!("{BAG_REFUSAL} at {}", t.position(*span)))
}),
BagMember::Num(_) => members[1..]
.iter()
.find(|(m, _)| !matches!(m, BagMember::Num(_)))
.map(|(m, span)| {
own_refusal(m, span).unwrap_or_else(|| match m {
BagMember::Null => format!(
"One or more parameter values were invalid: Numeric sets may not \
contain null under root{path}"
),
_ => BAG_REFUSAL.to_string(),
})
}),
_ => Some(format!("{BAG_REFUSAL} at {}", t.position(*first_span))),
};
if let Some(message) = refusal {
t.note_literal(message);
return Ok(AttributeValue::SS(Vec::new()));
}
let set = if strings {
AttributeValue::SS(
members
.into_iter()
.filter_map(|(m, _)| match m {
BagMember::Str(s) => Some(s),
_ => None,
})
.collect(),
)
} else {
AttributeValue::NS(
members
.into_iter()
.filter_map(|(m, _)| match m {
BagMember::Num(n) => Some(n),
_ => None,
})
.collect(),
)
};
t.note_set_literal(&set);
Ok(set)
}
fn read_bag_member(t: &mut Tokenizer, depth: usize) -> Result<BagMember, String> {
let tok = t.peek_token()?.ok_or("Unexpected end of set literal")?;
if tok == "?" {
t.next_token()?;
t.next_param_index();
return Ok(BagMember::Parameter);
}
if tok.starts_with('`') {
t.next_token()?;
return ion_type(&tok).map(BagMember::Ion).ok_or_else(String::new);
}
if tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2 {
t.next_token()?;
return Ok(BagMember::Str(tok[1..tok.len() - 1].to_string()));
}
if is_keyword(&tok, "NULL") {
t.next_token()?;
return Ok(BagMember::Null);
}
if tok.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
&& !is_any(&tok, &["TRUE", "FALSE"])
{
t.next_token()?;
parse_dotted_path_from_token(&tok, t)?;
return Ok(BagMember::Other);
}
Ok(match parse_value(t, depth + 1)? {
PartiqlValue::Literal(AttributeValue::N(n)) => BagMember::Num(n),
_ => BagMember::Other,
})
}
fn member_span(t: &Tokenizer, start: usize, member: &BagMember) -> Span {
let first = t.span_at(start);
let token = |i: usize| t.tokens.get(i).map(String::as_str);
if matches!(member, BagMember::Num(_)) && matches!(token(start), Some("-" | "+")) {
let number = t.span_at(start + 1);
return Span {
start: first.start,
len: number.start + number.len - first.start,
};
}
if token(start) == Some("<") {
return Span {
start: first.start,
len: 2,
};
}
first
}
fn expect_separator(t: &mut Tokenizer, close: &str, missing: &str) -> Result<(), String> {
match t.peek_token()?.as_deref() {
Some(",") => Ok(()),
Some(tok) if tok == close => Ok(()),
_ => Err(missing.to_string()),
}
}
fn ion_literal(t: &mut Tokenizer, tok: &str) -> Result<AttributeValue, String> {
let kind = ion_type(tok).ok_or_else(String::new)?;
let path: String = t.value_path.concat();
let at = t.position(t.span_at(t.pos - 1));
t.note_literal(format!(
"Ion Literals are not supported: {kind} under key root{path} at {at}"
));
Ok(AttributeValue::NULL(true))
}
fn ion_type(literal: &str) -> Option<&'static str> {
let text = literal.strip_prefix('`')?.strip_suffix('`')?.trim();
if let Some(inner) = text.strip_prefix("{{") {
let inner = inner.strip_suffix("}}")?.trim();
if inner.starts_with('"') || inner.starts_with("'''") {
return Some("CLOB");
}
let base64 = inner.chars().all(|c| {
c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '=') || c.is_ascii_whitespace()
});
return base64.then_some("BLOB");
}
let wrapped = |open: char, close: char| {
text.len() >= 2 && text.starts_with(open) && text.ends_with(close)
};
if wrapped('{', '}') {
return Some("STRUCT");
}
if wrapped('[', ']') {
return Some("LIST");
}
if wrapped('(', ')') {
return Some("SEXP");
}
if wrapped('"', '"') || (text.len() >= 6 && text.starts_with("'''") && text.ends_with("'''")) {
return Some("STRING");
}
if wrapped('\'', '\'') {
return Some("SYMBOL");
}
match text {
"null" => return Some("NULL"),
"true" | "false" => return Some("BOOL"),
"nan" | "+inf" | "-inf" => return Some("FLOAT"),
_ => {}
}
if let Some(kind) = text.strip_prefix("null.") {
return kind
.chars()
.all(|c| c.is_ascii_alphabetic())
.then_some("NULL");
}
if is_ion_timestamp(text) {
return Some("TIMESTAMP");
}
if let Some(kind) = ion_number(text) {
return Some(kind);
}
let symbol = text.starts_with(|c: char| c.is_ascii_alphabetic() || matches!(c, '_' | '$'))
&& text
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '$'));
symbol.then_some("SYMBOL")
}
fn is_ion_timestamp(text: &str) -> bool {
let bytes = text.as_bytes();
bytes.len() >= 5
&& bytes[..4].iter().all(u8::is_ascii_digit)
&& matches!(bytes[4], b'-' | b'T')
&& text
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, '-' | ':' | 'T' | 'Z' | '.' | '+'))
}
fn ion_number(text: &str) -> Option<&'static str> {
let unsigned = text.strip_prefix('-').unwrap_or(text);
let digits = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit() || c == '_');
let radix = [("0x", 16), ("0X", 16), ("0b", 2), ("0B", 2)];
if let Some((rest, base)) = radix
.iter()
.find_map(|(prefix, base)| unsigned.strip_prefix(prefix).map(|rest| (rest, *base)))
{
let ok = !rest.is_empty() && rest.chars().all(|c| c == '_' || c.is_digit(base));
return ok.then_some("INT");
}
let (mantissa, exponent, float) = if let Some(at) = unsigned.find(['e', 'E']) {
(&unsigned[..at], Some(&unsigned[at + 1..]), true)
} else if let Some(at) = unsigned.find(['d', 'D']) {
(&unsigned[..at], Some(&unsigned[at + 1..]), false)
} else {
(unsigned, None, false)
};
if let Some(exponent) = exponent
&& !digits(exponent.strip_prefix(['+', '-']).unwrap_or(exponent))
{
return None;
}
let (whole, fraction) = match mantissa.split_once('.') {
Some((whole, fraction)) => (whole, Some(fraction)),
None => (mantissa, None),
};
if !digits(whole) || fraction.is_some_and(|f| !f.is_empty() && !digits(f)) {
return None;
}
Some(if float {
"FLOAT"
} else if fraction.is_some() || exponent.is_some() {
"DECIMAL"
} else {
"INT"
})
}
fn parse_item_literal(
t: &mut Tokenizer,
depth: usize,
) -> Result<HashMap<String, AttributeValue>, String> {
let open = t.next_token()?.ok_or("Expected '{'")?;
if open != "{" {
return Err(format!("Expected '{{' but got '{open}'"));
}
let mut item = HashMap::new();
loop {
let tok = t.peek_token()?.ok_or("Unexpected end of item literal")?;
if tok == "}" {
t.next_token()?; break;
}
if tok == "," {
t.next_token()?;
continue;
}
let key_tok = t.next_token()?.ok_or("Expected key in item literal")?;
let key = unquote(&key_tok);
t.note_key(&key);
let colon = t.next_token()?.ok_or("Expected ':'")?;
if colon != ":" {
return Err(format!("Expected ':' but got '{colon}'"));
}
t.value_path.push(format!(".{key}"));
let val = parse_item_value(t, depth);
t.value_path.pop();
item.insert(key, val?);
expect_separator(t, "}", "Expected RIGHT_CURLY")?;
}
Ok(item)
}
fn parse_item_value(t: &mut Tokenizer, depth: usize) -> Result<AttributeValue, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
t.note_depth();
let tok = t.peek_token()?.ok_or("Expected value")?;
if tok == "{" {
let inner = parse_item_literal(t, depth + 1)?;
return Ok(AttributeValue::M(inner));
}
if tok == "[" {
t.next_token()?; let mut items = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of list")?;
if peek == "]" {
t.next_token()?;
break;
}
if peek == "," {
t.next_token()?;
continue;
}
t.value_path.push(format!("[{}]", items.len()));
let item = parse_item_value(t, depth + 1);
t.value_path.pop();
items.push(item?);
expect_separator(t, "]", "Expected RIGHT_BRACKET")?;
}
return Ok(AttributeValue::L(items));
}
if tok == "<"
&& t.peek_token_at(1)?.as_deref() == Some("<>")
&& t.peek_token_at(2)?.as_deref() == Some(">")
{
for _ in 0..3 {
t.next_token()?;
}
t.note_empty_bag();
return Ok(AttributeValue::SS(Vec::new()));
}
if tok == "<" && t.peek_token_at(1)?.as_deref() == Some("<") {
t.next_token()?; t.next_token()?; return parse_bag(t, depth);
}
let tok = t.next_token()?.ok_or("Expected value")?;
if tok.starts_with('`') {
return ion_literal(t, &tok);
}
if tok.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
&& t.peek_token()?.as_deref() == Some("(")
{
let index = t.pos - 1;
let name = if depth == 0 {
tok.to_lowercase()
} else {
"Call".to_string()
};
let path: String = t.value_path.concat();
let at = t.position(t.span_at(index));
t.note_literal(format!(
"Unsupported data type: {name} under key root{path} at {at}"
));
let mut open = 0usize;
loop {
match t.next_token()?.as_deref() {
Some("(") => open += 1,
Some(")") if open == 1 => break,
Some(")") => open -= 1,
Some(_) => {}
None => return Err("Expected ')'".to_string()),
}
}
return Ok(AttributeValue::NULL(true));
}
if tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2 {
return Ok(AttributeValue::S(tok[1..tok.len() - 1].to_string()));
}
if (tok == "-" || tok == "+")
&& let Some(ref next) = t.peek_token()?
&& is_number_token(next)
{
let num = t.next_token()?.unwrap();
let text = format!("{tok}{num}");
t.note_number(&text);
return Ok(AttributeValue::N(text));
}
if is_number_token(&tok) {
t.note_number(&tok);
return Ok(AttributeValue::N(tok));
}
match tok.to_uppercase().as_str() {
"TRUE" => Ok(AttributeValue::BOOL(true)),
"FALSE" => Ok(AttributeValue::BOOL(false)),
"NULL" => Ok(AttributeValue::NULL(true)),
_ if tok.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
&& t.peek_token()?.is_some_and(|next| next.starts_with('\'')) =>
{
Ok(AttributeValue::NULL(true))
}
_ => Err(format!("Unexpected value in item literal: {tok}")),
}
}
fn parse_item_literal_partiql(
t: &mut Tokenizer,
depth: usize,
) -> Result<HashMap<String, PartiqlValue>, String> {
let open = t.next_token()?.ok_or("Expected '{'")?;
if open != "{" {
return Err(format!("Expected '{{' but got '{open}'"));
}
let mut item = HashMap::new();
loop {
let tok = t.peek_token()?.ok_or("Unexpected end of item literal")?;
if tok == "}" {
t.next_token()?; break;
}
if tok == "," {
t.next_token()?;
continue;
}
let key_tok = t.next_token()?.ok_or("Expected key in item literal")?;
let key = unquote(&key_tok);
t.note_key(&key);
let colon = t.next_token()?.ok_or("Expected ':'")?;
if colon != ":" {
return Err(format!("Expected ':' but got '{colon}'"));
}
let nested = depth > 0;
if nested {
t.value_path.push(format!(".{key}"));
}
let val = parse_item_value_partiql(t, depth);
if nested {
t.value_path.pop();
}
item.insert(key, val?);
expect_separator(t, "}", "Expected RIGHT_CURLY")?;
}
Ok(item)
}
fn parse_item_value_partiql(t: &mut Tokenizer, depth: usize) -> Result<PartiqlValue, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
t.note_depth();
let tok = t.peek_token()?.ok_or("Expected value")?;
if tok == "?" {
t.next_token()?; let idx = t.next_param_index();
return Ok(PartiqlValue::Parameter(idx));
}
if tok == "[" {
return parse_value(t, depth + 1);
}
if tok == "{" {
let inner = parse_item_literal_partiql(t, depth + 1)?;
let mut map = HashMap::new();
for (k, v) in inner {
match v {
PartiqlValue::Literal(av) => {
map.insert(k, av);
}
PartiqlValue::Parameter(_) => {
return Err(
"Parameter placeholders inside nested map literals are not yet fully supported"
.to_string(),
);
}
}
}
return Ok(PartiqlValue::Literal(AttributeValue::M(map)));
}
let av = parse_item_value(t, depth)?;
Ok(PartiqlValue::Literal(av))
}
fn unquote(s: &str) -> String {
if s.len() >= 2
&& ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
{
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
fn expect_keyword(t: &mut Tokenizer, kw: &str) -> Result<(), String> {
let tok = t.next_token()?.ok_or(format!("Expected '{kw}'"))?;
if !tok.eq_ignore_ascii_case(kw) {
return Err(format!("Expected '{kw}' but got '{tok}'"));
}
Ok(())
}
struct Tokenizer {
tokens: Vec<String>,
spans: Vec<Span>,
input: String,
pos: usize,
param_counter: usize,
clause: LiteralClause,
value_path: Vec<String>,
refusals: Vec<Refusal>,
operand_refusal: Option<String>,
depth: usize,
operators: usize,
reading_where: bool,
reading_literal: usize,
}
struct Refusal {
clause: LiteralClause,
message: String,
literal: bool,
}
const LITERAL_NESTING_LIMIT: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LiteralClause {
Where,
Set,
Value,
}
impl Tokenizer {
fn new(input: &str) -> Result<Self, String> {
let (tokens, spans) = tokenize(input)?;
Ok(Self {
tokens,
spans,
input: input.to_string(),
pos: 0,
param_counter: 0,
clause: LiteralClause::Where,
value_path: Vec::new(),
refusals: Vec::new(),
operand_refusal: None,
depth: 0,
operators: 0,
reading_where: false,
reading_literal: 0,
})
}
fn note_unrecognized_function(&mut self, index: usize) {
if self.clause_refused() {
return;
}
let name = self.tokens[index].to_lowercase();
let at = self.position(self.span_at(index));
self.note_term(format!("Unrecognized function: {name} at {at}"));
}
fn note_term(&mut self, message: String) {
if self.clause_refused() {
return;
}
self.refusals.push(Refusal {
clause: self.clause,
message,
literal: false,
});
}
fn clause_refused(&self) -> bool {
self.refusals.iter().any(|r| r.clause == self.clause)
}
fn holds_refusal(&self) -> bool {
!self.refusals.is_empty() || self.operand_refusal.is_some()
}
fn count_operator(&mut self) {
self.operators += 1;
if self.operators == MAX_OPERATORS + 1 {
self.note_term(format!(
"The expression contains too many operators; operator count: {}",
self.operators
));
}
}
fn note_literal(&mut self, message: String) {
if self.clause_refused() {
return;
}
self.refusals.push(Refusal {
clause: self.clause,
message,
literal: true,
});
}
fn note_empty_bag(&mut self) {
self.note_literal("Empty bags are not supported".to_string());
}
fn note_set_literal(&mut self, value: &AttributeValue) {
if self.clause_refused() {
return;
}
let message = match value {
AttributeValue::NS(members) => {
let mut seen = std::collections::HashSet::new();
let repeated = members
.iter()
.any(|n| !seen.insert(crate::types::normalize_dynamo_number(n)));
repeated.then(|| {
let path: String = self.value_path.concat();
format!(
"One or more parameter values were invalid: Input collection [{}] \
contains duplicates! under root{path}",
members.join(", ")
)
})
}
AttributeValue::SS(_) => crate::validation::duplicate_set_message(
value,
crate::validation::ValueLayer::Older,
),
_ => None,
};
if let Some(message) = message {
self.note_literal(message);
}
}
fn note_operand_refusal(&mut self, message: String) {
self.operand_refusal.get_or_insert(message);
}
fn note_number(&mut self, text: &str) {
if self.clause_refused() {
return;
}
if let Err(message) = check_literal_number(text) {
let path: String = self.value_path.concat();
self.note_literal(format!("{message} under root{path}"));
}
}
fn note_key(&mut self, key: &str) {
if key.is_empty() {
self.note_literal("Path component cannot be an empty string".to_string());
}
}
fn note_depth(&mut self) {
if self.value_path.len() == LITERAL_NESTING_LIMIT && !self.clause_refused() {
let path: String = self.value_path.concat();
self.note_literal(format!(
"Nesting Levels have exceeded supported limits under root{path}"
));
}
}
fn first_refusal(&self) -> Option<ParseFailure> {
[
LiteralClause::Where,
LiteralClause::Set,
LiteralClause::Value,
]
.into_iter()
.find_map(|clause| self.refusals.iter().find(|r| r.clause == clause))
.map(|r| {
if r.literal {
ParseFailure::Literal(r.message.clone())
} else {
ParseFailure::Statement(ParseError::Validation(r.message.clone()))
}
})
.or_else(|| {
self.operand_refusal
.clone()
.map(|m| ParseFailure::Statement(ParseError::Validation(m)))
})
}
fn span_at(&self, index: usize) -> Span {
self.spans
.get(index)
.copied()
.unwrap_or(Span { start: 0, len: 0 })
}
fn position(&self, span: Span) -> String {
let (line, col) = span.line_col(&self.input);
format!("{line}:{col}:{}", span.len)
}
fn next_token(&mut self) -> Result<Option<String>, String> {
if self.pos >= self.tokens.len() {
return Ok(None);
}
let tok = self.tokens[self.pos].clone();
self.pos += 1;
if self.reading_where && self.reading_literal == 0 {
let calls = self.tokens.get(self.pos).is_some_and(|next| next == "(");
if is_comparison_operator(&tok)
|| is_any(&tok, &["NOT", "IN", "BETWEEN", "IS"])
|| (calls && WHERE_FUNCTIONS.iter().any(|f| tok.eq_ignore_ascii_case(f)))
{
self.count_operator();
}
}
Ok(Some(tok))
}
fn peek_token(&self) -> Result<Option<String>, String> {
if self.pos >= self.tokens.len() {
return Ok(None);
}
Ok(Some(self.tokens[self.pos].clone()))
}
fn peek_token_at(&self, offset: usize) -> Result<Option<String>, String> {
let idx = self.pos + offset;
if idx >= self.tokens.len() {
return Ok(None);
}
Ok(Some(self.tokens[idx].clone()))
}
fn next_param_index(&mut self) -> usize {
let idx = self.param_counter;
self.param_counter += 1;
idx
}
}
#[derive(Debug, Clone, Copy)]
struct Span {
start: usize,
len: usize,
}
impl Span {
fn line_col(&self, input: &str) -> (usize, usize) {
let (mut line, mut col) = (1usize, 1usize);
for c in input.chars().take(self.start) {
if c == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
}
fn emit(tokens: &mut Vec<String>, spans: &mut Vec<Span>, tok: String, start: usize, end: usize) {
spans.push(Span {
start,
len: end - start,
});
tokens.push(tok);
}
fn tokenize(input: &str) -> Result<(Vec<String>, Vec<Span>), String> {
let chars: Vec<char> = input.chars().collect();
let len = chars.len();
let mut tokens = Vec::new();
let mut spans = Vec::with_capacity(len / 4 + 1);
let mut i = 0;
while i < len {
if chars[i].is_ascii_whitespace() {
i += 1;
continue;
}
if chars[i] == '-' && chars.get(i + 1) == Some(&'-') {
while i < len && chars[i] != '\n' {
i += 1;
}
continue;
}
if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
match (i + 2..len.saturating_sub(1)).find(|&j| chars[j] == '*' && chars[j + 1] == '/') {
Some(close) => {
i = close + 2;
continue;
}
None => return Err(String::new()),
}
}
if chars[i] == '#' {
return Err(String::new());
}
let start = i;
let leading_point = chars[i] == '.'
&& chars.get(i + 1).is_some_and(|c| c.is_ascii_digit())
&& !(i > 0
&& (chars[i - 1].is_ascii_alphanumeric()
|| matches!(chars[i - 1], '_' | '"' | ']' | ')')));
match chars[i] {
'.' if leading_point => {}
'{' | '}' | '[' | ']' | '(' | ')' | ',' | ':' | '*' | '?' | '+' | '-' | '.' | ';' => {
let tok = chars[i].to_string();
i += 1;
emit(&mut tokens, &mut spans, tok, start, i);
continue;
}
_ => {}
}
if i + 1 < len {
let two = format!("{}{}", chars[i], chars[i + 1]);
match two.as_str() {
"<>" | "<=" | ">=" | "!=" => {
i += 2;
emit(&mut tokens, &mut spans, two, start, i);
continue;
}
_ => {}
}
}
if matches!(chars[i], '=' | '<' | '>') {
let tok = chars[i].to_string();
i += 1;
emit(&mut tokens, &mut spans, tok, start, i);
continue;
}
if chars[i] == '`' {
let Some(close) = (i + 1..len).find(|&j| chars[j] == '`') else {
return Err(String::new());
};
i = close + 1;
let tok: String = chars[start..i].iter().collect();
emit(&mut tokens, &mut spans, tok, start, i);
continue;
}
if chars[i] == '\'' {
let mut s = String::from('\'');
i += 1;
while i < len {
if chars[i] == '\'' {
if i + 1 < len && chars[i + 1] == '\'' {
s.push('\'');
i += 2;
} else {
break; }
} else {
s.push(chars[i]);
i += 1;
}
}
if i >= len {
return Err("Unterminated string literal".to_string());
}
s.push('\'');
i += 1;
emit(&mut tokens, &mut spans, s, start, i);
continue;
}
if chars[i] == '"' {
let mut s = String::from('"');
i += 1;
while i < len {
if chars[i] == '"' {
if i + 1 < len && chars[i + 1] == '"' {
s.push('"');
i += 2;
} else {
break; }
} else {
s.push(chars[i]);
i += 1;
}
}
if i >= len {
return Err("Unterminated quoted identifier".to_string());
}
s.push('"');
i += 1;
emit(&mut tokens, &mut spans, s, start, i);
continue;
}
if chars[i].is_ascii_digit() || leading_point {
let mut s = String::new();
let mut seen_point = false;
while i < len && (chars[i].is_ascii_digit() || (chars[i] == '.' && !seen_point)) {
seen_point |= chars[i] == '.';
s.push(chars[i]);
i += 1;
}
if i < len && matches!(chars[i], 'e' | 'E') {
let sign = usize::from(i + 1 < len && matches!(chars[i + 1], '+' | '-'));
let first_digit = i + 1 + sign;
if first_digit >= len
|| !(chars[first_digit].is_ascii_alphanumeric() || chars[first_digit] == '_')
{
return Err(String::new());
}
if chars[first_digit].is_ascii_digit() {
let mut end = first_digit;
while end < len && chars[end].is_ascii_digit() {
end += 1;
}
let exponent: String = chars[i + 1..end].iter().collect();
let Ok(exponent) = exponent.parse::<i32>() else {
return Err(String::new());
};
let fraction_digits =
s.split_once('.').map_or(0, |(_, fraction)| fraction.len());
let scale = i64::try_from(fraction_digits)
.unwrap_or(i64::MAX)
.saturating_sub(i64::from(exponent));
if i32::try_from(scale).is_err() {
return Err(String::new());
}
s.extend(&chars[i..end]);
i = end;
}
}
emit(&mut tokens, &mut spans, s, start, i);
continue;
}
if chars[i].is_ascii_alphabetic() || chars[i] == '_' {
let mut s = String::new();
while i < len && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
s.push(chars[i]);
i += 1;
}
emit(&mut tokens, &mut spans, s, start, i);
continue;
}
if !chars[i].is_ascii() {
return Err(String::new());
}
return Err(format!("Unexpected character: '{}'", chars[i]));
}
Ok((tokens, spans))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_long_run_of_nots_is_refused_by_the_operator_limit() {
let statement = format!(r#"SELECT * FROM "t" WHERE {}pk='1'"#, "NOT ".repeat(5_000));
match parse(&statement) {
Err(ParseError::Validation(message)) => assert_eq!(
message,
"The expression contains too many operators; operator count: 301"
),
other => panic!("expected the operator limit, got {other:?}"),
}
}
#[test]
fn runs_of_not_between_brackets_stay_shallow() {
let run = "NOT ".repeat(MAX_OPERATORS + 1);
let statement = format!(
r#"SELECT * FROM "t" WHERE {}{}a=1{}"#,
format!("{run}(").repeat(MAX_EXPR_DEPTH),
run,
")".repeat(MAX_EXPR_DEPTH)
);
let outcome = std::thread::Builder::new()
.stack_size(2 * 1024 * 1024)
.spawn(move || parse(&statement).map(|_| ()).map_err(|e| e.to_string()))
.unwrap()
.join()
.expect("the parser ran out of stack");
let err = outcome.expect_err("past the operator limit");
assert!(err.contains("too many operators"), "got {err}");
}
fn operators_in(clause: &str) -> usize {
let mut t = Tokenizer::new(clause).unwrap();
let _ = parse_conditions_with_or(&mut t);
t.operators
}
#[test]
fn a_bag_literal_counts_no_operators() {
assert_eq!(operators_in("pk = 'a' AND s = <<'x'>>"), 3);
assert_eq!(operators_in("pk = 'a' AND s = <<'x', 'y'>>"), 3);
assert_eq!(operators_in("pk = 'a' AND attribute_exists(<<'x'>>)"), 3);
assert_eq!(operators_in("pk = 'a' AND s IN [<<'x'>>, <<'y'>>]"), 3);
}
#[test]
fn a_run_of_nots_keeps_its_parity() {
for (nots, negated) in [(1, true), (2, false), (3, true), (4, false)] {
let statement = format!(r#"SELECT * FROM "t" WHERE {}a=1"#, "NOT ".repeat(nots));
let Statement::Select {
where_clause: Some(wc),
..
} = parse(&statement).unwrap()
else {
panic!("expected a SELECT with WHERE");
};
let is_negated = match &*wc.groups[0][0] {
WhereCondition::Comparison(_) => false,
WhereCondition::NotComparison(_) => true,
other => panic!("{nots} NOTs: {other:?}"),
};
assert_eq!(is_negated, negated, "{nots} NOTs");
}
}
#[test]
fn a_clause_too_wide_to_flatten_is_rejected_rather_than_grinding() {
let values = (0..MAX_IN_OPERANDS)
.map(|i| format!("'v{i}'"))
.collect::<Vec<_>>()
.join(",");
let mut clause = format!("NOT a IN ({values})");
for i in 0..8 {
clause.push_str(&format!(" AND NOT (b='p{i}' AND b='q{i}')"));
}
let start = std::time::Instant::now();
let err = parse(&format!(r#"SELECT * FROM "t" WHERE {clause}"#)).unwrap_err();
assert!(err.to_string().contains("too complex"), "got {err}");
assert!(start.elapsed() < std::time::Duration::from_secs(5));
}
#[test]
fn a_negated_in_past_100_operands_is_refused_for_them() {
for path in ["a".repeat(4_000), "a".to_string()] {
let values = vec!["1"; 1_900].join(",");
let statement =
format!(r#"SELECT * FROM "t" WHERE NOT {path} IN [{values}] AND (x = 1 OR x = 2)"#);
assert!(statement.chars().count() <= 8_192, "{}", statement.len());
let err = parse(&statement).unwrap_err().to_string();
assert!(
err.contains(
"The IN operator is provided with too many operands; number of operands: 1900"
),
"got {err}"
);
}
}
#[test]
fn a_negated_in_of_100_operands_is_evaluated() {
for path in ["a".repeat(4_000), "a".to_string()] {
let values = vec!["1"; MAX_IN_OPERANDS].join(",");
let statement =
format!(r#"SELECT * FROM "t" WHERE NOT {path} IN [{values}] AND (x = 1 OR x = 2)"#);
assert!(parse(&statement).is_ok(), "{} path", path.len());
}
}
#[test]
fn a_negated_in_whose_copies_would_pass_the_budget_is_too_complex() {
let path = "a".repeat(MAX_CLAUSE_BYTES / MAX_IN_OPERANDS + 1);
let values = vec!["1"; MAX_IN_OPERANDS].join(",");
let err = parse(&format!(
r#"SELECT * FROM "t" WHERE NOT {path} IN [{values}]"#
))
.unwrap_err();
assert!(err.to_string().contains("too complex"), "got {err}");
}
#[test]
fn a_wide_alternation_is_refused_without_flattening_it() {
let clause = (0..40_000)
.map(|i| format!("a='v{i}'"))
.collect::<Vec<_>>()
.join(" OR ");
let start = std::time::Instant::now();
let err = parse(&format!(r#"SELECT * FROM "t" WHERE {clause}"#)).unwrap_err();
assert!(err.to_string().contains("too many operators"), "got {err}");
assert!(start.elapsed() < std::time::Duration::from_secs(5));
}
fn leaf(name: &str) -> ConditionTree {
ConditionTree::Leaf(Arc::new(WhereCondition::Exists(name.to_string())))
}
#[test]
fn flattening_refuses_past_its_caps_before_building() {
let pair = || ConditionTree::Or(vec![leaf("a"), leaf("b")]);
assert_eq!(
to_dnf(&ConditionTree::And(vec![pair(); 8])).unwrap().len(),
256
);
assert_eq!(
to_dnf(&ConditionTree::And(vec![pair(); 9])).unwrap_err(),
TOO_COMPLEX
);
let alternation = |n| ConditionTree::Or((0..n).map(|_| leaf("a")).collect());
assert_eq!(
to_dnf(&alternation(MAX_OR_GROUPS)).unwrap().len(),
MAX_OR_GROUPS
);
assert_eq!(
to_dnf(&alternation(MAX_OR_GROUPS + 1)).unwrap_err(),
TOO_COMPLEX
);
let conjunction = |n| ConditionTree::And((0..n).map(|_| leaf("a")).collect());
assert_eq!(
slots(&to_dnf(&conjunction(MAX_TOTAL_CONDITIONS)).unwrap()),
MAX_TOTAL_CONDITIONS
);
assert_eq!(
to_dnf(&conjunction(MAX_TOTAL_CONDITIONS + 1)).unwrap_err(),
TOO_COMPLEX
);
let wide = ConditionTree::And((0..300).map(|_| leaf("w")).collect());
let mut terms = vec![pair(); 4];
terms.push(wide);
terms.push(pair());
assert_eq!(to_dnf(&ConditionTree::And(terms)).unwrap_err(), TOO_COMPLEX);
}
#[test]
fn an_alternation_is_held_to_four_times_the_slot_budget() {
let conjunction = |n| ConditionTree::And((0..n).map(|_| leaf("a")).collect());
let mut arms = vec![conjunction(MAX_TOTAL_CONDITIONS); 4];
assert_eq!(
slots(&to_dnf(&ConditionTree::Or(arms.clone())).unwrap()),
4 * MAX_TOTAL_CONDITIONS
);
arms.push(leaf("b"));
assert_eq!(to_dnf(&ConditionTree::Or(arms)).unwrap_err(), TOO_COMPLEX);
}
#[test]
fn flattening_shares_each_condition_across_groups() {
let shared = leaf("s");
let pair = ConditionTree::Or(vec![leaf("a"), leaf("b")]);
let groups = to_dnf(&ConditionTree::And(vec![pair, shared])).unwrap();
assert_eq!(groups.len(), 2);
assert!(Arc::ptr_eq(&groups[0][1], &groups[1][1]));
}
#[test]
fn the_byte_budget_counts_each_condition_once() {
let repeated = format!(
r#"SELECT * FROM "t" WHERE pk = 'k' AND {} AND s = '{}'"#,
["(a = 1 OR a = 2)"; 8].join(" AND "),
"x".repeat(4_070)
);
let Statement::Select {
where_clause: Some(wc),
..
} = parse(&repeated).unwrap()
else {
panic!("expected a SELECT with WHERE");
};
let per_copy: usize = wc.groups.iter().flatten().map(|c| condition_bytes(c)).sum();
let held = wc.tree.as_ref().unwrap().bytes();
assert!(per_copy > 1024 * 1024, "{per_copy}");
assert!(held < 8_192, "{held}");
let at_budget = format!(
r#"SELECT * FROM "t" WHERE s = '{}'"#,
"x".repeat(MAX_CLAUSE_BYTES - 1)
);
assert!(parse(&at_budget).is_ok());
let over = format!(
r#"SELECT * FROM "t" WHERE s = '{}'"#,
"x".repeat(MAX_CLAUSE_BYTES)
);
let err = parse(&over).unwrap_err();
assert!(err.to_string().contains("too complex"), "got {err}");
}
#[test]
fn only_the_first_refusal_in_a_clause_is_kept() {
let mut t = Tokenizer::new("SELECT * FROM \"t\"").unwrap();
t.clause = LiteralClause::Value;
t.value_path = vec![format!(".{}", "k".repeat(10_000)); 31];
for _ in 0..1_000 {
t.note_number("1e999999999");
t.note_key("");
t.note_depth();
t.note_empty_bag();
}
assert_eq!(t.refusals.len(), 1);
assert!(
t.refusals[0].message.contains("under root.kkk"),
"{}",
&t.refusals[0].message[..80]
);
t.clause = LiteralClause::Where;
t.note_key("");
assert_eq!(t.refusals.len(), 2);
assert_eq!(
t.refusals[1].message,
"Path component cannot be an empty string"
);
}
#[test]
fn test_parse_select_star() {
let stmt = parse("SELECT * FROM \"TestTable\"").unwrap();
match stmt {
Statement::Select {
table_name,
projections,
where_clause,
..
} => {
assert_eq!(table_name, "TestTable");
assert!(projections.is_empty());
assert!(where_clause.is_none());
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_select_with_where() {
let stmt = parse("SELECT * FROM \"T\" WHERE pk = 'hello'").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &*wc.groups[0][0] {
WhereCondition::Comparison(c) => {
assert_eq!(c.path, "pk");
assert_eq!(c.op, CompOp::Eq);
}
_ => panic!("Expected Comparison"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_select_with_projection() {
let stmt = parse("SELECT name, age FROM \"Users\"").unwrap();
match stmt {
Statement::Select { projections, .. } => {
assert_eq!(projections, vec!["name", "age"]);
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_insert() {
let stmt =
parse("INSERT INTO \"TestTable\" VALUE {'pk': 'key1', 'data': 'hello'}").unwrap();
match stmt {
Statement::Insert {
table_name, item, ..
} => {
assert_eq!(table_name, "TestTable");
assert_eq!(
item.get("pk"),
Some(&PartiqlValue::Literal(AttributeValue::S(
"key1".to_string()
)))
);
assert_eq!(
item.get("data"),
Some(&PartiqlValue::Literal(AttributeValue::S(
"hello".to_string()
)))
);
}
_ => panic!("Expected INSERT"),
}
}
#[test]
fn test_parse_update() {
let stmt = parse("UPDATE \"T\" SET name = 'Bob' WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Update {
table_name,
set_clauses,
where_clause,
..
} => {
assert_eq!(table_name, "T");
assert_eq!(set_clauses.len(), 1);
assert_eq!(set_clauses[0].path, "name");
assert!(where_clause.is_some());
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn test_parse_delete() {
let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Delete {
table_name,
where_clause,
returning,
..
} => {
assert_eq!(table_name, "T");
assert!(where_clause.is_some());
assert!(returning.is_none());
}
_ => panic!("Expected DELETE"),
}
}
#[test]
fn test_parse_delete_returning_all_old() {
let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD *").unwrap();
match stmt {
Statement::Delete {
table_name,
where_clause,
returning,
..
} => {
assert_eq!(table_name, "T");
assert!(where_clause.is_some());
assert_eq!(returning, Some(ReturningVariant::AllOld));
}
_ => panic!("Expected DELETE"),
}
}
#[test]
fn test_parse_delete_returning_is_case_insensitive() {
let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1' returning all old *").unwrap();
match stmt {
Statement::Delete { returning, .. } => {
assert_eq!(returning, Some(ReturningVariant::AllOld))
}
_ => panic!("Expected DELETE"),
}
}
#[test]
fn test_parse_delete_returning_recognises_all_variants() {
let cases = [
("RETURNING ALL OLD *", ReturningVariant::AllOld),
("RETURNING MODIFIED OLD *", ReturningVariant::ModifiedOld),
("RETURNING ALL NEW *", ReturningVariant::AllNew),
("RETURNING MODIFIED NEW *", ReturningVariant::ModifiedNew),
];
for (clause, expected) in cases {
let stmt = parse(&format!("DELETE FROM \"T\" WHERE pk = 'k1' {clause}")).unwrap();
match stmt {
Statement::Delete { returning, .. } => {
assert_eq!(returning, Some(expected), "clause {clause}")
}
_ => panic!("Expected DELETE for {clause}"),
}
}
}
#[test]
fn test_parse_update_returning_recognises_all_variants() {
let cases = [
("RETURNING ALL OLD *", ReturningVariant::AllOld),
("RETURNING MODIFIED OLD *", ReturningVariant::ModifiedOld),
("RETURNING ALL NEW *", ReturningVariant::AllNew),
("RETURNING MODIFIED NEW *", ReturningVariant::ModifiedNew),
];
for (clause, expected) in cases {
let stmt = parse(&format!(
"UPDATE \"T\" SET data = 'x' WHERE pk = 'k1' {clause}"
))
.unwrap();
match stmt {
Statement::Update { returning, .. } => {
assert_eq!(returning, Some(expected), "clause {clause}")
}
_ => panic!("Expected UPDATE for {clause}"),
}
}
}
#[test]
fn test_parse_delete_returning_rejects_incomplete_clause() {
assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD").is_err());
assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING").is_err());
assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD attr").is_err());
}
#[test]
fn test_parse_returning_rejects_invalid_keyword_pairs() {
for clause in [
"RETURNING OLD ALL *",
"RETURNING NEW MODIFIED *",
"RETURNING ALL ALL *",
"RETURNING FOO BAR *",
] {
let err = parse(&format!("DELETE FROM \"T\" WHERE pk = 'k1' {clause}")).unwrap_err();
assert!(
err.to_string().contains("Unsupported RETURNING clause"),
"clause {clause} gave unexpected error: {err}"
);
}
}
#[test]
fn test_parse_parameter() {
let stmt = parse("SELECT * FROM \"T\" WHERE pk = ?").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => match &*wc.groups[0][0] {
WhereCondition::Comparison(c) => match &c.value {
PartiqlValue::Parameter(0) => {}
other => panic!("Expected Parameter(0), got {other:?}"),
},
_ => panic!("Expected Comparison"),
},
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_numeric_literal() {
let stmt = parse("SELECT * FROM \"T\" WHERE age > 42").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => match &*wc.groups[0][0] {
WhereCondition::Comparison(c) => {
assert_eq!(c.op, CompOp::Gt);
match &c.value {
PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "42"),
other => panic!("Expected N(42), got {other:?}"),
}
}
_ => panic!("Expected Comparison"),
},
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_insert_with_number() {
let stmt = parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'age': 25}").unwrap();
match stmt {
Statement::Insert { item, .. } => {
assert_eq!(
item.get("age"),
Some(&PartiqlValue::Literal(AttributeValue::N("25".to_string())))
);
}
_ => panic!("Expected INSERT"),
}
}
#[test]
fn test_invalid_statement() {
let result = parse("MERGE INTO \"T\"");
assert!(result.is_err());
}
#[test]
fn test_empty_statement() {
let result = parse("");
assert!(result.is_err());
}
#[test]
fn test_parse_between() {
let stmt = parse("SELECT * FROM \"T\" WHERE age BETWEEN 18 AND 65").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &*wc.groups[0][0] {
WhereCondition::Between(path, low, high) => {
assert_eq!(path, "age");
match low {
PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "18"),
other => panic!("Expected N(18), got {other:?}"),
}
match high {
PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "65"),
other => panic!("Expected N(65), got {other:?}"),
}
}
other => panic!("Expected Between, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_between_and_other_condition() {
let stmt = parse("SELECT * FROM \"T\" WHERE x BETWEEN 1 AND 10 AND y = 'hello'").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 2);
assert!(matches!(&*wc.groups[0][0], WhereCondition::Between(..)));
assert!(matches!(&*wc.groups[0][1], WhereCondition::Comparison(..)));
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_in() {
let stmt = parse("SELECT * FROM \"T\" WHERE status IN ('ACTIVE', 'PENDING')").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &*wc.groups[0][0] {
WhereCondition::In(path, values) => {
assert_eq!(path, "status");
assert_eq!(values.len(), 2);
}
other => panic!("Expected In, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_contains() {
let stmt = parse("SELECT * FROM \"T\" WHERE CONTAINS(name, 'john')").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &*wc.groups[0][0] {
WhereCondition::Contains(path, val) => {
assert_eq!(path, "name");
match val {
PartiqlValue::Literal(AttributeValue::S(s)) => {
assert_eq!(s, "john")
}
other => panic!("Expected S(john), got {other:?}"),
}
}
other => panic!("Expected Contains, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_is_missing() {
let stmt = parse("SELECT * FROM \"T\" WHERE email IS MISSING").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &*wc.groups[0][0] {
WhereCondition::IsMissing(path) => assert_eq!(path, "email"),
other => panic!("Expected IsMissing, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_is_not_missing() {
let stmt = parse("SELECT * FROM \"T\" WHERE email IS NOT MISSING").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &*wc.groups[0][0] {
WhereCondition::IsNotMissing(path) => assert_eq!(path, "email"),
other => panic!("Expected IsNotMissing, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_nested_projection() {
let stmt = parse("SELECT a.b.c, d FROM \"T\"").unwrap();
match stmt {
Statement::Select { projections, .. } => {
assert_eq!(projections, vec!["a.b.c", "d"]);
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_array_index_projection() {
let stmt = parse("SELECT items[0].name FROM \"T\"").unwrap();
match stmt {
Statement::Select { projections, .. } => {
assert_eq!(projections, vec!["items[0].name"]);
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_update_with_remove() {
let stmt =
parse("UPDATE \"T\" SET name = 'Bob' REMOVE age, email WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Update {
set_clauses,
remove_paths,
where_clause,
..
} => {
assert_eq!(set_clauses.len(), 1);
assert_eq!(remove_paths, vec!["age", "email"]);
assert!(where_clause.is_some());
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn test_parse_update_remove_only() {
let stmt = parse("UPDATE \"T\" REMOVE old_field WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Update {
set_clauses,
remove_paths,
..
} => {
assert!(set_clauses.is_empty());
assert_eq!(remove_paths, vec!["old_field"]);
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn test_parse_set_expression_add() {
let stmt = parse("UPDATE \"T\" SET count = count + 1 WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Update { set_clauses, .. } => {
assert_eq!(set_clauses.len(), 1);
match &set_clauses[0].value {
SetValue::Add(attr, val) => {
assert_eq!(attr, "count");
assert_eq!(
val,
&PartiqlValue::Literal(AttributeValue::N("1".to_string()))
);
}
other => panic!("Expected Add, got {other:?}"),
}
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn test_parse_count_star_is_a_parse_error() {
assert!(parse("SELECT COUNT(*) FROM \"T\"").is_err());
}
#[test]
fn test_count_rejection_reports_the_token_position() {
assert_eq!(
count_projection_rejection("SELECT COUNT(*) FROM \"T\""),
Some("Unexpected path component at 1:8:5".to_string())
);
assert_eq!(
count_projection_rejection("SELECT count(pk) FROM \"T\""),
Some("Unexpected path component at 1:9:5".to_string())
);
}
#[test]
fn test_count_rejection_tracks_lines() {
assert_eq!(
count_projection_rejection("SELECT\n COUNT(*) FROM \"T\""),
Some("Unexpected path component at 2:2:5".to_string())
);
}
#[test]
fn test_count_rejection_ignores_quotes_and_word_boundaries() {
assert_eq!(
count_projection_rejection("SELECT \"COUNT\" FROM \"T\""),
None
);
assert_eq!(
count_projection_rejection("SELECT 'count(' FROM \"T\""),
None
);
assert_eq!(
count_projection_rejection("SELECT recount(x) FROM \"T\""),
Some("Unexpected path component at 1:8:7".to_string())
);
}
#[test]
fn test_count_rejection_only_applies_before_from_on_a_select() {
assert_eq!(
count_projection_rejection("UPDATE \"T\" SET count = count + 1 WHERE pk = 'k1'"),
None
);
assert_eq!(
count_projection_rejection("SELECT pk FROM \"T\" WHERE count(x)"),
None
);
}
#[test]
fn test_parse_set_literal() {
let stmt = parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'tags': <<'a', 'b'>>}").unwrap();
match stmt {
Statement::Insert { item, .. } => match item.get("tags") {
Some(PartiqlValue::Literal(AttributeValue::SS(ss))) => {
assert!(ss.contains(&"a".to_string()));
assert!(ss.contains(&"b".to_string()));
}
other => panic!("Expected SS, got {other:?}"),
},
_ => panic!("Expected INSERT"),
}
}
#[test]
fn test_parse_or_condition() {
let stmt = parse("SELECT * FROM \"T\" WHERE status = 'A' OR status = 'B'").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups.len(), 2);
assert_eq!(wc.groups[0].len(), 1);
assert_eq!(wc.groups[1].len(), 1);
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_and_or_mixed() {
let stmt = parse("SELECT * FROM \"T\" WHERE a = 1 AND b = 2 OR c = 3").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups.len(), 2);
assert_eq!(wc.groups[0].len(), 2); assert_eq!(wc.groups[1].len(), 1); }
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_insert_if_not_exists() {
let stmt =
parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'name': 'A'} IF NOT EXISTS").unwrap();
match stmt {
Statement::Insert { if_not_exists, .. } => {
assert!(if_not_exists);
}
_ => panic!("Expected INSERT"),
}
}
#[test]
fn test_parse_nested_path_in_where_function() {
let stmt = parse("SELECT * FROM \"T\" WHERE BEGINS_WITH(address.city, 'Lon')").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => match &*wc.groups[0][0] {
WhereCondition::BeginsWith(path, _) => {
assert_eq!(path, "address.city");
}
other => panic!("Expected BeginsWith, got {other:?}"),
},
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn select_index_qualifier_keeps_the_where_clause() {
let stmt = parse("SELECT * FROM \"T\".\"idx\" WHERE pk = 'p'").unwrap();
match stmt {
Statement::Select {
table_name,
where_clause,
..
} => {
assert_eq!(table_name, "T");
assert!(
where_clause.is_some(),
"the qualifier stranded the WHERE clause"
);
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn delete_index_qualifier_keeps_the_where_clause() {
let stmt = parse("DELETE FROM \"T\".\"idx\" WHERE pk = 'p'").unwrap();
match stmt {
Statement::Delete {
table_name,
where_clause,
..
} => {
assert_eq!(table_name, "T");
assert!(
where_clause.is_some(),
"the qualifier stranded the WHERE clause"
);
}
_ => panic!("Expected DELETE"),
}
}
#[test]
fn update_index_qualifier_keeps_its_clauses() {
let stmt = parse("UPDATE \"T\".\"idx\" SET a = 'x' WHERE pk = 'p'")
.expect("the qualifier stranded the SET clause");
match stmt {
Statement::Update {
table_name,
set_clauses,
where_clause,
..
} => {
assert_eq!(table_name, "T");
assert_eq!(set_clauses.len(), 1);
assert!(where_clause.is_some());
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn select_index_qualifier_is_captured() {
let stmt = parse("SELECT * FROM \"T\".\"idx\"").unwrap();
assert_eq!(index_name(&stmt), Some("idx"));
assert_eq!(table_name(&stmt), Some("T"));
}
#[test]
fn select_without_a_qualifier_carries_no_index() {
let stmt = parse("SELECT * FROM \"T\" WHERE pk = 'p'").unwrap();
assert_eq!(index_name(&stmt), None);
}
#[test]
fn unquoted_index_qualifier_parses() {
let stmt = parse("SELECT * FROM T.idx").unwrap();
assert_eq!(table_name(&stmt), Some("T"));
assert_eq!(index_name(&stmt), Some("idx"));
}
#[test]
fn write_statements_carry_the_qualifier_for_the_executor_to_reject() {
let stmt = parse("UPDATE \"T\".\"idx\" SET a = 'x' WHERE pk = 'p'").unwrap();
assert_eq!(index_name(&stmt), Some("idx"));
let stmt = parse("DELETE FROM \"T\".\"idx\" WHERE pk = 'p'").unwrap();
assert_eq!(index_name(&stmt), Some("idx"));
}
#[test]
fn insert_rejects_a_qualifier_and_reports_the_table_name_position() {
let err = parse("INSERT INTO \"abcdefgh\".\"idx\" VALUE {'pk':'a'}").unwrap_err();
assert_eq!(
err,
ParseError::Validation(
"FROM clause may only contain a single table name in data \
manipulation statements at 1:13:10"
.to_string()
)
);
let err = parse("INSERT INTO \"abcdefgh\".\"idx\" VALUE {'pk':'a'}").unwrap_err();
assert!(err.to_string().ends_with("at 1:17:10"), "got {err}");
let err = parse("INSERT INTO\n\"abcdefgh\".\"idx\" VALUE {'pk':'a'}").unwrap_err();
assert!(err.to_string().ends_with("at 2:1:10"), "got {err}");
let err = parse("INSERT INTO abcdefgh.idx VALUE {'pk':'a'}").unwrap_err();
assert!(err.to_string().ends_with("at 1:13:8"), "got {err}");
}
#[test]
fn insert_without_a_qualifier_is_unaffected() {
let stmt = parse("INSERT INTO \"T\" VALUE {'pk':'a'}").unwrap();
assert_eq!(table_name(&stmt), Some("T"));
assert_eq!(index_name(&stmt), None);
}
#[test]
fn an_unterminated_quote_is_rejected_rather_than_panicking() {
for stmt in [
"SELECT * FROM \"",
"SELECT * FROM \"T\".\"",
"SELECT * FROM \"unterminated",
"SELECT * FROM \"T\" WHERE pk = 'oops",
] {
let err = parse(stmt).unwrap_err();
assert!(
matches!(err, ParseError::Syntax(_)),
"{stmt:?} should be a syntax error, got {err:?}"
);
}
}
#[test]
fn unquote_is_total() {
for input in ["", "\"", "'", "\"\"", "''", "\"a\"", "a"] {
let _ = unquote(input);
}
}
#[test]
fn a_trailing_dot_with_no_index_name_is_an_error() {
assert!(parse("SELECT * FROM \"T\".").is_err());
}
#[test]
fn an_empty_index_component_is_rejected() {
let err = parse("SELECT * FROM \"T\".\"\"").unwrap_err();
assert_eq!(
err,
ParseError::Validation("Path component cannot be an empty string".to_string())
);
}
#[test]
fn a_three_part_name_is_rejected() {
let err = parse("SELECT * FROM \"T\".\"idx\".\"more\"").unwrap_err();
assert_eq!(
err,
ParseError::Validation(
"A path may contain at most 2 components in the FROM clause".to_string()
)
);
}
#[test]
fn a_number_with_an_exponent_is_one_token() {
for input in ["1e5", "1E5", "1e+5", "1e-5", "1.5e3", "15E-1"] {
let (tokens, _) = tokenize(input).unwrap();
assert_eq!(tokens, vec![input.to_string()], "{input}");
}
}
#[test]
fn an_e_without_exponent_digits_is_not_part_of_the_number() {
for (input, expected) in [
("1ex", vec!["1", "ex"]),
("1 e5", vec!["1", "e5"]),
("e5", vec!["e5"]),
] {
let (tokens, _) = tokenize(input).unwrap();
assert_eq!(tokens, expected, "{input}");
}
for input in ["1e", "1e+", "1e )"] {
assert_eq!(tokenize(input).unwrap_err(), String::new(), "{input}");
}
}
#[test]
fn an_exponent_too_large_to_read_is_a_syntax_error_with_no_detail() {
for input in ["1e99999999999", "1e-99999999999", "2e2147483648"] {
assert_eq!(
parse(&format!("SELECT * FROM \"T\" WHERE pk = {input}")).unwrap_err(),
ParseError::Syntax(String::new()),
"{input}"
);
}
assert!(matches!(
parse("SELECT * FROM \"T\" WHERE pk = 1e2147483647"),
Err(ParseError::Validation(message)) if message.starts_with("Number underflow")
));
}
#[test]
fn a_token_left_after_a_statement_is_a_syntax_error() {
for input in [
"SELECT * FROM \"T\" WHERE pk = 1 foo",
"DELETE FROM \"T\" WHERE pk = 1 foo",
"UPDATE \"T\" SET a = 1 WHERE pk = 1 foo",
"INSERT INTO \"T\" VALUE {'pk': 1} foo",
] {
assert_eq!(
parse(input).unwrap_err(),
ParseError::Syntax("Unexpected token after expression".to_string()),
"{input}"
);
}
}
#[test]
fn an_order_by_clause_on_a_select_is_accepted() {
for input in [
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY sk",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY sk DESC",
"SELECT * FROM \"T\" WHERE pk = 1 order by sk asc",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY sk DESC, other ASC",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY a.b[0]",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY \"sk\"",
"SELECT * FROM \"T\" ORDER BY sk",
] {
assert!(parse(input).is_ok(), "{input}");
}
assert!(parse("DELETE FROM \"T\" WHERE pk = 1 ORDER BY sk").is_err());
}
fn order_by_of(input: &str) -> Vec<(String, bool)> {
match parse(input).unwrap() {
Statement::Select { order_by, .. } => order_by
.into_iter()
.map(|term| (term.path, term.descending))
.collect(),
other => panic!("expected a SELECT, got {other:?}"),
}
}
#[test]
fn order_by_terms_are_carried_on_the_select() {
assert_eq!(
order_by_of("SELECT * FROM \"T\" WHERE pk = 1 ORDER BY sk DESC, other"),
vec![("sk".to_string(), true), ("other".to_string(), false)]
);
assert_eq!(
order_by_of("SELECT * FROM \"T\" WHERE pk = 1 order by \"sk\" asc"),
vec![("sk".to_string(), false)]
);
assert!(order_by_of("SELECT * FROM \"T\" WHERE pk = 1").is_empty());
}
fn where_value_of(input: &str) -> PartiqlValue {
match parse(input).unwrap() {
Statement::Select {
where_clause: Some(wc),
..
} => match &*wc.groups[0][0] {
WhereCondition::Comparison(c) => c.value.clone(),
other => panic!("expected a comparison, got {other:?}"),
},
other => panic!("expected a SELECT with a WHERE, got {other:?}"),
}
}
#[test]
fn a_point_starts_a_number_only_where_a_value_can_start() {
for (input, expected) in [
("SELECT * FROM \"T\" WHERE pk = .5e1", ".5e1"),
("SELECT * FROM \"T\" WHERE pk=.5", ".5"),
("SELECT * FROM \"T\" WHERE pk = -.5e1", "-.5e1"),
] {
assert_eq!(
where_value_of(input),
PartiqlValue::Literal(AttributeValue::N(expected.to_string())),
"{input}"
);
}
match parse("SELECT a.b, \"c\".d, l[0].e FROM \"T\"").unwrap() {
Statement::Select { projections, .. } => {
assert_eq!(projections, vec!["a.b", "c.d", "l[0].e"]);
}
other => panic!("expected a SELECT, got {other:?}"),
}
}
#[test]
fn an_order_by_clause_must_be_whole_and_last() {
for input in [
"SELECT * FROM \"T\" ORDER BY sk WHERE pk = 'x'",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY sk DESC junk",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY sk ASC DESC",
] {
assert_eq!(
parse(input).unwrap_err(),
ParseError::Syntax("Unexpected token after expression".to_string()),
"{input}"
);
}
for input in [
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY sk,",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER sk",
"SELECT * FROM \"T\" WHERE pk = 1 ORDER BY DESC",
] {
assert!(
matches!(parse(input), Err(ParseError::Syntax(_))),
"{input}"
);
}
}
#[test]
fn comments_are_read_as_whitespace() {
let plain = format!("{:?}", parse("SELECT * FROM \"T\" WHERE pk = 1").unwrap());
for input in [
"SELECT * FROM \"T\" WHERE pk = 1 -- note",
"SELECT * FROM \"T\" WHERE pk = 1 --",
"SELECT * FROM \"T\" -- c\nWHERE pk = 1",
"-- lead\nSELECT * FROM \"T\" WHERE pk = 1",
"SELECT * FROM \"T\" WHERE pk = 1 /* c */",
"SELECT * FROM \"T\" /* c */ WHERE pk = 1",
"SELECT/**/* FROM \"T\" WHERE pk = 1",
] {
let parsed = parse(input).unwrap_or_else(|e| panic!("{input:?}: {e:?}"));
assert_eq!(format!("{parsed:?}"), plain, "{input:?}");
}
let (tokens, _) = tokenize("'a -- b' \"c /* d */\"").unwrap();
assert_eq!(tokens, vec!["'a -- b'", "\"c /* d */\""]);
}
#[test]
fn a_hash_or_an_unterminated_block_comment_is_a_syntax_error_with_no_detail() {
for input in [
"SELECT * FROM \"T\" WHERE pk = 1 # c",
"SELECT * FROM \"T\" WHERE pk = 1 /* unterminated",
"SELECT * FROM \"T\" WHERE pk = 1 /*/",
] {
assert_eq!(
parse(input).unwrap_err(),
ParseError::Syntax(String::new()),
"{input:?}"
);
}
}
#[test]
fn an_exponent_whose_scale_does_not_fit_an_i32_is_a_syntax_error_with_no_detail() {
for input in [
"1e-2147483648",
"1.5e-2147483647",
"0.1e-2147483647",
"1.50e-2147483646",
"0e-2147483648",
] {
assert_eq!(
parse(&format!("SELECT * FROM \"T\" WHERE pk = {input}")).unwrap_err(),
ParseError::Syntax(String::new()),
"{input}"
);
}
for input in ["1.5e-2147483646", "10e-2147483647"] {
let (tokens, _) = tokenize(input).unwrap();
assert_eq!(tokens, vec![input.to_string()], "{input}");
}
}
#[test]
fn an_alias_after_the_table_name_is_refused() {
for input in [
"SELECT * FROM \"T\" t WHERE pk = 1",
"SELECT * FROM \"T\" AS t WHERE pk = 1",
"SELECT * FROM \"T\" t",
"SELECT * FROM \"T\" foo",
"SELECT * FROM \"T\" as t",
"SELECT t.pk FROM \"T\" t WHERE t.pk = 1",
"DELETE FROM \"T\" t WHERE pk = 5",
"UPDATE \"T\" t SET a = 3 WHERE pk = 1",
] {
assert_eq!(
parse(input).unwrap_err(),
ParseError::Validation("Aliasing is not supported".to_string()),
"{input}"
);
}
assert_eq!(
parse("INSERT INTO \"T\" t VALUE {'pk': 8}").unwrap_err(),
ParseError::Syntax("Unexpected token after expression".to_string())
);
for input in [
"SELECT * FROM \"T\" WHERE pk = 1",
"SELECT * FROM \"T\" ORDER BY pk",
"DELETE FROM \"T\" WHERE pk = 5",
"UPDATE \"T\" SET a = 3 WHERE pk = 1",
"UPDATE \"T\" REMOVE a WHERE pk = 1",
] {
assert!(parse(input).is_ok(), "{input}");
}
}
#[test]
fn the_parser_refuses_nested_size_calls_without_the_length_limit() {
let statement = format!(
r#"SELECT * FROM "Tbl" WHERE {}a{} = 1"#,
"size(".repeat(10_000),
")".repeat(10_000)
);
assert!(parse(&statement).is_err());
}
}