use super::{owned::Lowerer, ExpressionPosition};
use crate::ast::*;
use hawdb_core::{HawDBError, Result, Value};
use hawdb_sql_syntax::{
self as syntax,
peg::{Grammar, Limits, NodeId, ParseTree},
Token, TokenKind,
};
use std::sync::OnceLock;
mod grammar;
fn semantic(message: impl Into<String>) -> HawDBError {
HawDBError::Semantic(message.into())
}
pub(super) fn parse(input: &str) -> Result<SqlStatement> {
static GRAMMAR: OnceLock<Grammar> = OnceLock::new();
let grammar =
GRAMMAR.get_or_init(|| grammar::build().expect("valid executable SQL PEG grammar"));
let tokens = syntax::tokenize_with_config(
input,
syntax::LexerConfig {
max_input_bytes: usize::MAX,
..syntax::LexerConfig::default()
},
)
.map_err(parse_error)?;
let tree = grammar
.parse(
input,
&tokens,
"HawInput",
Limits {
depth: 8192,
..Limits::default()
},
)
.map_err(parse_error)?;
syntax::peg::validate_sql_expression_depth(&tree, &tokens)
.map_err(|_| semantic("SQL expression nesting limit exceeded"))?;
let fold = Fold {
input,
tokens: &tokens,
tree: &tree,
lowerer: Lowerer::for_statement(input, &tokens),
};
fold.statement(fold.first(fold.first(tree.root)))
}
fn parse_error(error: syntax::SyntaxError) -> HawDBError {
if error.code == syntax::SyntaxErrorCode::InvalidParameter {
semantic("PostgreSQL parameters must use one-based $n syntax")
} else {
HawDBError::Parse(error.to_string())
}
}
struct Fold<'a> {
input: &'a str,
tokens: &'a [Token],
tree: &'a ParseTree,
lowerer: Lowerer<'a>,
}
impl Fold<'_> {
fn first(&self, node: NodeId) -> NodeId {
self.tree.children(node)[0]
}
fn child(&self, node: NodeId, name: &str) -> NodeId {
self.tree.required(node, name)
}
fn has(&self, node: NodeId, name: &str) -> bool {
self.tree.child(node, name).is_some()
}
fn children(&self, node: NodeId, name: &str) -> Vec<NodeId> {
self.tree
.children(node)
.iter()
.copied()
.filter(|id| self.tree.nodes[*id].name == Some(name))
.collect()
}
fn text(&self, node: NodeId) -> &str {
let span = self.tree.span(node, self.tokens);
&self.input[span.start..span.end]
}
fn ident(&self, node: NodeId) -> String {
let token = self.tokens[self.tree.nodes[node].start];
self.lowerer.ident(&syntax::Identifier {
span: token.span,
quoted: token.kind == TokenKind::QuotedIdentifier,
})
}
fn name(&self, node: NodeId) -> syntax::QualifiedName {
syntax::QualifiedName {
span: self.tree.span(node, self.tokens),
parts: self
.tree
.children(node)
.iter()
.map(|id| {
let token = self.tokens[self.tree.nodes[*id].start];
syntax::Identifier {
span: token.span,
quoted: token.kind == TokenKind::QuotedIdentifier,
}
})
.collect(),
}
}
fn table(&self, node: NodeId) -> Result<SqlTableName> {
self.lowerer.name(&self.name(node))
}
fn unqualified(&self, node: NodeId, message: &str) -> Result<String> {
let name = self.name(node);
let [part] = name.parts.as_slice() else {
return Err(semantic(message));
};
Ok(self.lowerer.ident(part))
}
fn names(&self, node: NodeId, message: &str) -> Result<Vec<String>> {
self.tree
.children(node)
.iter()
.map(|id| self.unqualified(*id, message))
.collect()
}
fn expr(&self, node: NodeId) -> syntax::ExpressionSyntax {
syntax::peg::sql_expression(self.tree, self.tokens, node)
}
fn alias(&self, node: NodeId) -> Result<Option<String>> {
self.tree
.child(node, "Alias")
.or_else(|| self.tree.child(node, "RelationalAsAlias"))
.map(|alias| {
if self.has(alias, "Identifiers") {
return Err(semantic(
"PostgreSQL table column aliases are not supported",
));
}
Ok(self.ident(self.child(alias, "Identifier")))
})
.transpose()
}
fn selection(&self, node: NodeId) -> Result<Option<crate::Expr>> {
self.tree
.child(node, "Where")
.map(|id| {
self.lowerer.lower(
&self.expr(self.child(id, "Or")),
ExpressionPosition::Predicate,
)
})
.transpose()
}
fn statement(&self, node: NodeId) -> Result<SqlStatement> {
match self.tree.nodes[node].name {
Some("HawInsert") => self.insert(node),
Some("HawUpdate") => self.update(node),
Some("HawDelete") => self.delete(node),
Some("HawCreateTable") => self.create_table(node),
Some("HawCreateIndex") => self.create_index(node),
Some("HawCreateSearchIndex") => self.create_search_index(node),
Some("HawDropSearchIndex") => self.drop_search_index(node),
Some("HawAlter") => self.alter(node),
Some("HawCreateView") => self.create_view(node),
Some("HawDrop") => self.drop_view(node),
_ => Err(semantic("unsupported PostgreSQL statement kind")),
}
}
fn assignments(&self, node: NodeId) -> Result<Vec<SqlAssignment>> {
self.tree
.children(node)
.iter()
.map(|id| {
if self.has(*id, "HawTupleTarget") {
return Err(semantic("tuple assignments are not supported"));
}
let column = self.unqualified(
self.child(*id, "Name"),
"assignment targets must be unqualified columns",
)?;
let value = self.assignment_value(&self.expr(self.child(*id, "Or")))?;
Ok(SqlAssignment { column, value })
})
.collect()
}
fn assignment_value(&self, expr: &syntax::ExpressionSyntax) -> Result<SqlAssignmentValue> {
let expression = self
.lowerer
.lower(expr, super::ExpressionPosition::Scalar)?;
match expression.kind {
ExprKind::Column(column) => Ok(SqlAssignmentValue::Column(column)),
ExprKind::Value(value) => Ok(SqlAssignmentValue::Value(value)),
_ => Ok(SqlAssignmentValue::Expression(expression)),
}
}
fn insert_value(&self, node: NodeId) -> Result<SqlValue> {
let matched = self.first(node);
if self.tree.nodes[matched].name != Some("HawInsertLiteral") {
return self.lowerer.literal(&self.expr(matched));
}
let mut expression = self.expr(self.first(matched));
if self.tokens[self.tree.nodes[matched].start].kind == TokenKind::Minus {
expression = syntax::ExpressionSyntax {
span: self.tree.span(matched, self.tokens),
kind: syntax::ExpressionKindSyntax::Unary {
operator: syntax::UnaryOperatorSyntax::Minus,
expression: Box::new(expression),
},
};
}
self.lowerer.literal(&expression)
}
fn insert(&self, node: NodeId) -> Result<SqlStatement> {
let mut unsupported = Vec::new();
for (name, diagnostic) in [
("HawTableKeyword", "TABLE keyword"),
("HawInsertAlias", "table alias"),
("HawMutationTail", "additional clause"),
] {
if self.has(node, name) {
unsupported.push(diagnostic);
}
}
if let Some(modifier) = self.tree.child(node, "HawInsertModifier") {
let token = self.tokens[self.tree.nodes[modifier].start];
unsupported.push(if token.is_keyword(self.input, "IGNORE") {
"IGNORE"
} else if token.is_keyword(self.input, "OVERWRITE") {
"OVERWRITE"
} else {
"OR conflict action"
});
}
if !self.has(node, "HawInto") {
unsupported.push("missing INTO");
}
super::clauses::reject("INSERT", &unsupported)?;
let source = self.first(self.child(node, "HawInsertSource"));
if self.tree.nodes[source].name == Some("HawDefaultValues") && self.has(node, "HawNames") {
return Err(HawDBError::Parse(
"explicit INSERT columns require VALUES or SELECT".into(),
));
}
if self.tree.nodes[source].name != Some("HawValues") {
return Err(semantic("INSERT supports VALUES rows only"));
}
let columns = self
.tree
.child(node, "HawNames")
.map(|id| self.names(id, "INSERT columns must be unqualified identifiers"))
.transpose()?
.unwrap_or_default();
let rows = self
.tree
.children(source)
.iter()
.map(|row| {
if self.has(*row, "HawExplicitRow") {
return Err(semantic("INSERT requires the PostgreSQL VALUES form"));
}
self.children(*row, "HawInsertValue")
.iter()
.map(|id| self.insert_value(*id))
.collect::<Result<Vec<_>>>()
})
.collect::<Result<Vec<_>>>()?;
let arity = columns.len().max(rows.first().map_or(0, Vec::len));
if rows.is_empty()
|| rows.iter().any(|row| row.len() != arity)
|| (!columns.is_empty() && arity != columns.len())
{
return Err(semantic(
"INSERT VALUES rows must have equal length and match any explicit column list",
));
}
let on_conflict = self
.tree
.child(node, "HawConflict")
.map(|id| self.conflict(id))
.transpose()?;
let returning = self
.tree
.child(node, "HawReturning")
.map(|id| {
self.tree
.children(id)
.iter()
.map(|item| {
if self.has(*item, "HawReturnAlias") {
return Err(semantic(
"INSERT RETURNING supports column references only",
));
}
self.lowerer
.column(&self.expr(self.child(*item, "Or")))
.map_err(|_| {
semantic("INSERT RETURNING supports column references only")
})
})
.collect::<Result<Vec<_>>>()
})
.transpose()?
.unwrap_or_default();
Ok(SqlStatement::Insert(InsertStatement {
table: self.table(self.child(node, "Name"))?,
columns,
rows,
on_conflict,
returning,
}))
}
fn conflict(&self, node: NodeId) -> Result<SqlOnConflict> {
let Some(target) = self.tree.child(node, "HawNames") else {
return Err(semantic("ON CONFLICT requires an explicit column target"));
};
let action = if self.has(node, "HawNothing") {
SqlConflictAction::DoNothing
} else {
let update = self.child(node, "HawConflictUpdate");
if self.has(update, "Where") {
return Err(semantic("ON CONFLICT DO UPDATE WHERE is not supported"));
}
SqlConflictAction::DoUpdate(self.assignments(self.child(update, "HawAssignments"))?)
};
Ok(SqlOnConflict {
columns: self.names(target, "ON CONFLICT columns must be unqualified")?,
action,
})
}
fn update(&self, node: NodeId) -> Result<SqlStatement> {
if [
"RelationalJoin",
"HawMutationFrom",
"HawReturning",
"Order",
"Limit",
"HawMutationTail",
]
.iter()
.any(|name| self.has(node, name))
{
return Err(semantic("unsupported PostgreSQL UPDATE clause"));
}
let alias = self
.tree
.child(node, "HawUpdateAlias")
.map(|id| self.alias(id))
.transpose()?
.flatten();
Ok(SqlStatement::Update(UpdateStatement {
table: self.table(self.child(node, "Name"))?,
alias,
assignments: self.assignments(self.child(node, "HawAssignments"))?,
selection: self.selection(node)?,
}))
}
fn delete(&self, node: NodeId) -> Result<SqlStatement> {
if [
"HawDeleteTargets",
"HawUsing",
"HawReturning",
"Order",
"Limit",
]
.iter()
.any(|name| self.has(node, name))
{
return Err(semantic("unsupported PostgreSQL DELETE clause"));
}
let sources = self.tree.children(self.child(node, "RelationalFrom"));
let [source] = sources else {
return Err(semantic("DELETE supports exactly one base table"));
};
if self.has(*source, "RelationalJoin") {
return Err(semantic("DELETE joins are not supported"));
}
let relation = self.first(self.child(*source, "RelationalFromItem"));
if self.has(relation, "TableArguments") {
return Err(semantic(
"PostgreSQL relational SQL supports base tables only",
));
}
Ok(SqlStatement::Delete(DeleteStatement {
table: self.table(self.child(relation, "Name"))?,
alias: self.alias(relation)?,
selection: self.selection(node)?,
}))
}
fn table_clause(&self, node: NodeId) -> &'static str {
match self.tokens[self.tree.nodes[node].start]
.text(self.input)
.to_ascii_uppercase()
.as_str()
{
"TEMP" | "TEMPORARY" => "TEMPORARY",
"OR" => "OR REPLACE",
"AS" => "AS query",
"ON" => "ON COMMIT",
"INHERITS" => "INHERITS",
"PARTITION" => "PARTITION BY",
"CLUSTER" => "CLUSTER BY",
"ORDER" => "ORDER BY",
"LIKE" => "LIKE",
"CLONE" => "CLONE",
"WITHOUT" => "WITHOUT ROWID",
"COMMENT" => "COMMENT",
"UNLOGGED" => "UNLOGGED",
"EXTERNAL" => "EXTERNAL",
"DYNAMIC" => "DYNAMIC",
"TRANSIENT" => "TRANSIENT",
"VOLATILE" => "VOLATILE",
"ICEBERG" => "ICEBERG",
"GLOBAL" => "GLOBAL",
"LOCAL" => "LOCAL",
_ => unreachable!("recognized CREATE TABLE clause"),
}
}
fn create_table(&self, node: NodeId) -> Result<SqlStatement> {
let mut unsupported = self
.children(node, "HawTableModifier")
.iter()
.map(|id| self.table_clause(*id))
.collect::<Vec<_>>();
unsupported.extend(
self.children(node, "HawTableTail")
.iter()
.map(|id| self.first(*id))
.filter(|id| self.tree.nodes[*id].name != Some("HawStorage"))
.map(|id| self.table_clause(id)),
);
super::clauses::reject("CREATE TABLE", &unsupported)?;
let mut storage = None;
for tail in self.children(node, "HawTableTail") {
let tail = self.first(tail);
if self.tree.nodes[tail].name != Some("HawStorage") {
return Err(semantic(format!(
"unsupported PostgreSQL CREATE TABLE clause: {}",
self.table_clause(tail)
)));
}
if storage.replace(self.storage(tail)?).is_some() {
return Err(semantic("duplicate CREATE TABLE storage options"));
}
}
let (mut columns, mut constraints) = (Vec::new(), Vec::new());
if let Some(items) = self.tree.child(node, "HawTableColumns") {
for item in self.tree.children(items) {
let item = self.first(*item);
if self.tree.nodes[item].name == Some("HawColumn") {
columns.push(self.column(item)?);
} else {
constraints.push(self.constraint(item)?);
}
}
}
Ok(SqlStatement::CreateTable(CreateTableStatement {
table: self.table(self.child(node, "Name"))?,
if_not_exists: self.has(node, "HawIfNotExists"),
columns,
constraints,
storage: storage.unwrap_or(SqlTableStorage::RowPage),
}))
}
fn data_type(&self, node: NodeId) -> Result<SqlDataType> {
if self.has(node, "HawArrayType") {
return Err(semantic("unsupported PostgreSQL array data type"));
}
let kind = self.first(node);
let raw = self.tokens[self.tree.nodes[kind].start]
.text(self.input)
.to_ascii_uppercase();
match self.tree.nodes[kind].name {
Some("HawIntegerType") => {
if !matches!(raw.as_str(), "BIGINT" | "INT8") && self.has(kind, "Number") {
return Err(semantic("integer aliases do not support a display width"));
}
Ok(SqlDataType::BigInt)
}
Some("HawDoubleType") => Ok(SqlDataType::DoublePrecision),
Some("HawTimestampType") => {
let tokens = &self.tokens[self.tree.nodes[kind].start..self.tree.nodes[kind].end];
let precision = match self.tree.child(kind, "Number") {
None => hawdb_core::TimestampPrecision::DEFAULT,
Some(number) => self
.text(number)
.parse::<u8>()
.ok()
.and_then(hawdb_core::TimestampPrecision::new)
.ok_or_else(|| {
semantic("TIMESTAMP precision must be an integer between 0 and 6")
})?,
};
let with = tokens
.iter()
.any(|token| token.is_keyword(self.input, "WITH"));
let without = tokens
.iter()
.any(|token| token.is_keyword(self.input, "WITHOUT"));
let tz = raw == "TIMESTAMPTZ";
if tz && (with || without) {
return Err(semantic(
"TIMESTAMPTZ does not accept a WITH or WITHOUT TIME ZONE clause",
));
}
Ok(SqlDataType::Timestamp(
hawdb_core::sql_timestamp::TimestampType {
zoned: tz || with,
precision,
},
))
}
Some("HawFloatType") => {
let widths = self.children(kind, "Number");
if let Some(width) = widths.first() {
let precision = self.text(*width).parse::<u64>().map_err(|_| {
HawDBError::Parse("FLOAT precision must be an unsigned integer".into())
})?;
if let Some(scale) = widths.get(1) {
self.text(*scale).parse::<i64>().map_err(|_| {
HawDBError::Parse("FLOAT scale must be an integer".into())
})?;
return Err(semantic("FLOAT scale is not supported"));
}
if !(25..=53).contains(&precision) {
return Err(semantic("FLOAT precision must be between 25 and 53"));
}
}
Ok(SqlDataType::DoublePrecision)
}
Some("HawTextType") => {
let tokens = &self.tokens[self.tree.nodes[kind].start..self.tree.nodes[kind].end];
if let Some(width) = self.tree.child(kind, "Number") {
let length = self.text(width).parse::<u64>().map_err(|_| {
HawDBError::Parse(
"varying character length must be an unsigned integer".into(),
)
})?;
if length == 0
|| tokens.iter().any(|token| {
token.is_keyword(self.input, "CHARACTERS")
|| token.is_keyword(self.input, "OCTETS")
})
{
return Err(semantic(
"varying character length must be positive and omit a unit",
));
}
} else if tokens
.iter()
.any(|token| token.kind == TokenKind::LeftParen)
{
return Err(semantic("varying character MAX length is not supported"));
}
Ok(SqlDataType::Text)
}
Some("HawSimpleType") => Ok(match raw.as_str() {
"BOOLEAN" | "BOOL" => SqlDataType::Boolean,
"FLOAT8" => SqlDataType::DoublePrecision,
"TEXT" => SqlDataType::Text,
"BYTEA" => SqlDataType::Bytea,
"UUID" => SqlDataType::Uuid,
_ => unreachable!("simple data type"),
}),
_ => {
let name = self.name(self.child(kind, "Name"));
if name.parts.len() == 1
&& matches!(
self.lowerer.ident(&name.parts[0]).as_str(),
"decimal" | "numeric" | "dec"
)
{
let parameters = if let Some(args) = self.tree.child(kind, "HawTypeArguments") {
let tokens = &self.tokens
[self.tree.nodes[args].start + 1..self.tree.nodes[args].end - 1];
if tokens
.iter()
.filter(|token| token.kind == TokenKind::Comma)
.count()
> 1
{
return Err(HawDBError::Parse(
"at most two numeric type parameters".into(),
));
}
let mut parameters = Vec::new();
for (index, part) in tokens
.split(|token| token.kind == TokenKind::Comma)
.enumerate()
{
let number = match part {
[number] if number.kind == TokenKind::Number => number,
[sign, number]
if index == 1
&& matches!(
sign.kind,
TokenKind::Minus | TokenKind::Plus
)
&& number.kind == TokenKind::Number =>
{
if sign.kind == TokenKind::Minus {
return Err(semantic("Decimal scale must be 0..precision"));
}
number
}
_ => {
return Err(HawDBError::Parse(
"invalid numeric type parameter".into(),
))
}
};
if !number
.text(self.input)
.bytes()
.all(|byte| byte.is_ascii_digit())
{
return Err(HawDBError::Parse(
"numeric type parameters must be integers".into(),
));
}
parameters.push(number.span);
}
parameters
} else {
Vec::new()
};
return self
.lowerer
.decimal_type(¶meters)
.map(SqlDataType::Decimal);
}
if name.parts.len() != 1 || self.lowerer.ident(&name.parts[0]) != "vector" {
return Err(semantic(format!(
"unsupported PostgreSQL data type {}",
self.text(node)
)));
}
let invalid = || semantic("VECTOR requires one dimension between 1 and 4294967295");
let args = self
.tree
.child(kind, "HawTypeArguments")
.ok_or_else(invalid)?;
let begin = self.tree.nodes[args].start;
let end = self.tree.nodes[args].end;
if end - begin != 3 || self.tokens[begin + 1].kind != TokenKind::Number {
return Err(invalid());
}
let raw = self.tokens[begin + 1].text(self.input);
if !raw.bytes().all(|b| b.is_ascii_digit()) {
return Err(invalid());
}
raw.parse::<u32>()
.ok()
.and_then(hawdb_core::VectorDimension::new)
.map(SqlDataType::Vector)
.ok_or_else(invalid)
}
}
}
fn column(&self, node: NodeId) -> Result<SqlColumnDefinition> {
let mut column = SqlColumnDefinition {
name: self.ident(self.child(node, "Identifier")),
data_type: self.data_type(self.child(node, "HawType"))?,
nullable: true,
default: None,
primary_key: false,
unique: false,
references: None,
};
for option in self.children(node, "HawColumnOption") {
if self.has(option, "HawNamedConstraint") {
return Err(semantic("named column constraints are not supported"));
}
let option = self.first(option);
match self.tree.nodes[option].name {
Some("HawNotNull") => column.nullable = false,
Some("HawNull") => column.nullable = true,
Some("HawPrimaryColumn") => {
column.primary_key = true;
column.nullable = false;
}
Some("HawUnique") => column.unique = true,
Some("HawReference") => column.references = Some(self.reference(option)?),
Some("HawDefault") => {
let expr = self.expr(self.child(option, "Or"));
column.default = Some(
match self.lowerer.lower(&expr, ExpressionPosition::Scalar)?.kind {
ExprKind::Value(SqlValue::Expression(expression))
if matches!(expression.kind, ExprKind::CurrentTimestamp(_)) =>
{
let ExprKind::CurrentTimestamp(kind) = expression.kind else {
unreachable!("matched CURRENT_TIMESTAMP default")
};
SqlColumnDefault::CurrentTimestamp(kind)
}
ExprKind::Value(value) => {
let mut clock = false;
if let SqlValue::Expression(expression) = &value {
expression.visit(&mut |child| {
clock |=
matches!(child.kind, ExprKind::CurrentTimestamp(_));
});
}
if clock {
return Err(semantic(
"CURRENT_TIMESTAMP, LOCALTIMESTAMP and now() column defaults must be the whole default expression",
));
}
SqlColumnDefault::Literal(value)
}
ExprKind::Function {
name,
arguments,
distinct: false,
filter: None,
} if name == "uuidv7" && arguments.is_empty() => {
SqlColumnDefault::UuidV7
}
ExprKind::Function { name, .. } => {
return Err(semantic(format!(
"unsupported PostgreSQL column default function {name}"
)))
}
ExprKind::Column(_) => {
return Err(semantic("column defaults cannot reference a column"))
}
_ => {
return Err(semantic(
"unsupported PostgreSQL column default expression",
))
}
},
);
}
_ => return Err(semantic("unsupported PostgreSQL column option")),
}
}
Ok(column)
}
fn reference(&self, node: NodeId) -> Result<SqlForeignKeyReference> {
if self.has(node, "HawReferenceUnsupported") {
return Err(semantic("unsupported FOREIGN KEY clause"));
}
let mut on_delete = None;
let mut on_update = None;
for action in self.children(node, "HawReferenceAction") {
let action = self.first(action);
let slot = if self.tree.nodes[action].name == Some("HawOnDelete") {
&mut on_delete
} else {
&mut on_update
};
let tokens = &self.tokens[self.tree.nodes[self.child(action, "HawAction")].start
..self.tree.nodes[self.child(action, "HawAction")].end];
let value = if tokens[0].is_keyword(self.input, "NO") {
SqlReferentialAction::NoAction
} else if tokens[0].is_keyword(self.input, "RESTRICT") {
SqlReferentialAction::Restrict
} else if tokens[0].is_keyword(self.input, "CASCADE") {
SqlReferentialAction::Cascade
} else if tokens[1].is_keyword(self.input, "NULL") {
SqlReferentialAction::SetNull
} else {
return Err(semantic("ON DELETE/UPDATE SET DEFAULT is not supported"));
};
if slot.replace(value).is_some() {
return Err(HawDBError::Parse("duplicate foreign-key action".into()));
}
}
Ok(SqlForeignKeyReference {
table: self.table(self.child(node, "Name"))?,
columns: self
.tree
.child(node, "HawNames")
.map(|id| self.names(id, "reference columns must be unqualified"))
.transpose()?
.unwrap_or_default(),
on_delete: on_delete.unwrap_or(SqlReferentialAction::NoAction),
on_update: on_update.unwrap_or(SqlReferentialAction::NoAction),
})
}
fn constraint(&self, node: NodeId) -> Result<SqlTableConstraint> {
if self.has(node, "HawNamedConstraint") {
return Err(semantic("named table constraints are not supported"));
}
let node = self.first(node);
match self.tree.nodes[node].name {
Some("HawPrimaryConstraint") | Some("HawUniqueConstraint") => {
if self.has(node, "HawNullsDistinct") {
return Err(semantic("unsupported UNIQUE clause"));
}
let columns = self.index_columns(self.child(node, "HawIndexColumns"))?;
if columns.iter().any(|c| {
c.direction != SqlOrderDirection::Asc || c.nulls != SqlNullOrder::DialectDefault
}) {
return Err(semantic(
"constraint columns do not support ordering options",
));
}
let columns = columns.into_iter().map(|c| c.column.name).collect();
Ok(
if self.tree.nodes[node].name == Some("HawPrimaryConstraint") {
SqlTableConstraint::PrimaryKey(columns)
} else {
SqlTableConstraint::Unique(columns)
},
)
}
Some("HawForeignConstraint") => Ok(SqlTableConstraint::ForeignKey {
columns: self.names(
self.child(node, "HawNames"),
"foreign-key columns must be unqualified",
)?,
reference: self.reference(self.child(node, "HawReference"))?,
}),
_ => Err(semantic(
"CHECK and inline index constraints are not supported",
)),
}
}
fn storage(&self, node: NodeId) -> Result<SqlTableStorage> {
if self.tree.children(node).is_empty() {
return Ok(SqlTableStorage::RowPage);
}
let (mut mode, mut partition, mut order, mut generated) = (None, None, None, None);
for option in self.tree.children(node) {
let name = self.ident(self.child(*option, "Identifier"));
let expr = self.expr(self.child(*option, "Or"));
let syntax::ExpressionKindSyntax::Literal(syntax::LiteralSyntax::String(span)) =
&expr.kind
else {
return Err(semantic(format!(
"CREATE TABLE {name} must be a string literal"
)));
};
if !self.input[span.start..span.end].starts_with('\'') {
return Err(semantic(format!(
"CREATE TABLE {name} must be a string literal"
)));
}
let SqlValue::Literal(Value::String(value)) = self.lowerer.literal(&expr)? else {
unreachable!("string literal");
};
let slot = match name.as_str() {
"storage_mode" => &mut mode,
"partition_key" => &mut partition,
"order_key" => &mut order,
"generated_order" => &mut generated,
_ => {
return Err(semantic(format!(
"unsupported PostgreSQL CREATE TABLE storage option {name}"
)))
}
};
if slot.replace(value).is_some() {
return Err(semantic(format!(
"PostgreSQL CREATE TABLE storage option {name} is specified more than once"
)));
}
}
let generated = generated
.map(|v| {
if v.eq_ignore_ascii_case("commit_sequence") {
Ok(SqlGeneratedOrder::CommitSequence)
} else {
Err(semantic(format!(
"unsupported strict_append generated_order {v}"
)))
}
})
.transpose()?;
match mode.as_deref().map(str::to_ascii_lowercase).as_deref() {
Some("strict_append")=>{
let key=|value:Option<String>,name:&str|->Result<Vec<String>> {let value=value.ok_or_else(||semantic(format!("strict_append storage requires {name}")))?;if value.is_empty() {return Err(semantic(format!("CREATE TABLE {name} must not contain an empty column name")));}Ok(vec![value])};
Ok(SqlTableStorage::StrictAppend {partition_key:key(partition,"partition_key")?,order_key:key(order,"order_key")?,generated_order:generated.unwrap_or_default()})
},
Some(mode)=>Err(semantic(format!("unsupported PostgreSQL CREATE TABLE storage_mode {mode}"))),
None if partition.is_none()&&order.is_none()&&generated.is_none()=>Ok(SqlTableStorage::RowPage),
None=>Err(semantic("partition_key, order_key, and generated_order require storage_mode = 'strict_append'")),
}
}
fn index_columns(&self, node: NodeId) -> Result<Vec<SqlIndexColumn>> {
self.tree
.children(node)
.iter()
.map(|id| {
if self.has(*id, "HawOperatorClass") {
return Err(semantic("index operator classes are not supported"));
}
let nulls = self
.tree
.child(*id, "NullOrder")
.map(|n| {
if self.has(n, "First") {
SqlNullOrder::First
} else {
SqlNullOrder::Last
}
})
.unwrap_or(SqlNullOrder::DialectDefault);
Ok(SqlIndexColumn {
column: self.lowerer.column(&self.expr(self.child(*id, "Or")))?,
direction: if self.has(*id, "Descending") {
SqlOrderDirection::Desc
} else {
SqlOrderDirection::Asc
},
nulls,
})
})
.collect()
}
fn create_index(&self, node: NodeId) -> Result<SqlStatement> {
if ["HawConcurrently", "HawIndexUsing", "HawIndexTail"]
.iter()
.any(|name| self.has(node, name))
{
return Err(semantic("unsupported PostgreSQL CREATE INDEX clause"));
}
let name = self
.tree
.child(node, "HawIndexName")
.ok_or_else(|| semantic("CREATE INDEX requires an explicit name"))?;
Ok(SqlStatement::CreateIndex(CreateIndexStatement {
name: self.unqualified(
self.child(name, "Name"),
"CREATE INDEX names must be unqualified",
)?,
table: self.table(self.child(node, "Name"))?,
columns: self.index_columns(self.child(node, "HawIndexColumns"))?,
unique: self.has(node, "HawUnique"),
if_not_exists: self.has(node, "HawIfNotExists"),
}))
}
fn create_search_index(&self, node: NodeId) -> Result<SqlStatement> {
let mut analyzer = None;
if let Some(options) = self.tree.child(node, "HawStorage") {
for option in self.tree.children(options) {
let name = self.ident(self.child(*option, "Identifier"));
let expr = self.expr(self.child(*option, "Or"));
let SqlValue::Literal(Value::String(value)) = self.lowerer.literal(&expr)? else {
return Err(semantic(format!(
"CREATE SEARCH INDEX {name} must be a string literal"
)));
};
if name != "analyzer" {
return Err(semantic(format!(
"unsupported CREATE SEARCH INDEX option {name}"
)));
}
if analyzer.replace(value).is_some() {
return Err(semantic(
"CREATE SEARCH INDEX analyzer is specified more than once",
));
}
}
}
let name = self.child(self.child(node, "HawSearchIndexName"), "Name");
Ok(SqlStatement::CreateSearchIndex(
CreateSearchIndexStatement {
name: self.table(name)?,
table: self.table(self.child(self.child(node, "HawSearchIndexTable"), "Name"))?,
column: self
.ident(self.child(self.child(node, "HawSearchIndexColumn"), "Identifier")),
analyzer,
if_not_exists: self.has(node, "HawIfNotExists"),
},
))
}
fn drop_search_index(&self, node: NodeId) -> Result<SqlStatement> {
Ok(SqlStatement::DropSearchIndex(DropSearchIndexStatement {
name: self.table(self.child(node, "Name"))?,
if_exists: self.has(node, "HawIfExists"),
}))
}
fn alter(&self, node: NodeId) -> Result<SqlStatement> {
if self.has(node, "HawIfExists") || self.has(node, "HawOnly") {
return Err(semantic("unsupported PostgreSQL ALTER TABLE clause"));
}
let operations = self.children(node, "HawAlterOperation");
let [operation] = operations.as_slice() else {
return Err(semantic("ALTER TABLE supports one ADD COLUMN operation"));
};
let operation = self.first(*operation);
if self.tree.nodes[operation].name != Some("HawAddColumn") {
return Err(semantic("ALTER TABLE supports one ADD COLUMN operation"));
}
if self.has(operation, "HawColumnPosition") {
return Err(semantic("ALTER TABLE column positioning is not supported"));
}
Ok(SqlStatement::AlterTableAddColumn(
AlterTableAddColumnStatement {
table: self.table(self.child(node, "Name"))?,
if_not_exists: self.has(operation, "HawIfNotExists"),
column: self.column(self.child(operation, "HawColumn"))?,
},
))
}
fn create_view(&self, node: NodeId) -> Result<SqlStatement> {
if !self.has(node, "HawMaterialized") {
return Err(semantic("only MATERIALIZED VIEW is supported"));
}
if ["HawOrReplace", "HawNames", "HawStorage", "HawViewTail"]
.iter()
.any(|name| self.has(node, name))
{
return Err(semantic(
"unsupported PostgreSQL CREATE MATERIALIZED VIEW clause",
));
}
let select = syntax::peg::sql_select(
self.input,
self.tree,
self.tokens,
self.child(node, "RelationalSelect"),
);
let SqlStatement::Select(query) = self.lowerer.select(&select)? else {
unreachable!("SELECT AST");
};
Ok(SqlStatement::CreateMaterializedView(
CreateMaterializedViewStatement {
name: self.table(self.child(node, "Name"))?,
if_not_exists: self.has(node, "HawIfNotExists"),
query: Box::new(query),
},
))
}
fn drop_view(&self, node: NodeId) -> Result<SqlStatement> {
if !self.has(node, "HawMaterialized")
|| !self
.text(self.child(node, "HawDropKind"))
.eq_ignore_ascii_case("VIEW")
{
return Err(semantic("unsupported PostgreSQL statement kind"));
}
let names = self.children(node, "Name");
if names.len() != 1
|| self
.tree
.child(node, "HawDropOption")
.is_some_and(|id| !self.text(id).eq_ignore_ascii_case("RESTRICT"))
{
return Err(semantic(
"unsupported PostgreSQL DROP MATERIALIZED VIEW clause",
));
}
Ok(SqlStatement::DropMaterializedView(
DropMaterializedViewStatement {
name: self.table(names[0])?,
if_exists: self.has(node, "HawIfExists"),
},
))
}
}