use powdb_storage::types::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum Statement {
Query(QueryExpr),
Insert(InsertExpr),
UpdateQuery(UpdateExpr),
DeleteQuery(DeleteExpr),
CreateType(CreateTypeExpr),
CreateLink(CreateLinkExpr),
DropLink(DropLinkExpr),
AlterTable(AlterTableExpr),
DropTable(DropTableExpr),
CreateView(CreateViewExpr),
RefreshView(RefreshViewExpr),
DropView(DropViewExpr),
Union(UnionExpr),
Upsert(UpsertExpr),
Explain(Box<Statement>),
Begin,
Commit,
Rollback,
ListTypes,
Describe(String),
ListLinks,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlterTableExpr {
pub table: String,
pub action: AlterAction,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateLinkExpr {
pub owner: String,
pub name: String,
pub target: String,
pub local_key: String,
pub target_key: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DropLinkExpr {
pub owner: String,
pub name: String,
pub if_exists: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ViaLink {
pub outer_alias: String,
pub link_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IndexTarget {
Column(String),
JsonPath(powdb_storage::stored_json_path::StoredJsonPathV1),
}
#[derive(Debug, Clone, PartialEq)]
pub enum AlterAction {
AddColumn {
name: String,
type_name: String,
required: bool,
},
DropColumn {
name: String,
if_exists: bool,
},
AddIndex {
target: IndexTarget,
if_not_exists: bool,
},
AddUnique {
target: IndexTarget,
if_not_exists: bool,
},
DropIndex {
target: IndexTarget,
if_exists: bool,
},
AddLink {
name: String,
target: String,
local_key: String,
target_key: String,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct DropTableExpr {
pub table: String,
pub if_exists: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CreateViewExpr {
pub name: String,
pub query: QueryExpr,
pub query_text: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RefreshViewExpr {
pub name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DropViewExpr {
pub name: String,
pub if_exists: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct UnionExpr {
pub left: Box<Statement>,
pub right: Box<Statement>,
pub all: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct QueryExpr {
pub source: String,
pub alias: Option<String>,
pub joins: Vec<JoinClause>,
pub filter: Option<Expr>,
pub order: Option<OrderClause>,
pub limit: Option<Expr>,
pub offset: Option<Expr>,
pub projection: Option<Vec<ProjectionField>>,
pub aggregation: Option<AggregateExpr>,
pub distinct: bool,
pub group_by: Option<GroupByClause>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GroupByClause {
pub keys: Vec<GroupKey>,
pub having: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GroupKey {
pub expr: Expr,
pub output_name: String,
}
impl GroupKey {
pub fn output_name(&self) -> String {
self.output_name.clone()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct JoinClause {
pub kind: JoinKind,
pub source: String,
pub alias: Option<String>,
pub on: Option<Expr>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinKind {
Inner,
LeftOuter,
RightOuter,
Cross,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProjectionField {
pub alias: Option<String>,
pub expr: Expr,
}
#[derive(Debug, Clone, PartialEq)]
pub struct NestedQuery {
pub source: String,
pub alias: String,
pub via_link: Option<ViaLink>,
pub filter: Expr,
pub order: Option<OrderClause>,
pub limit: Option<Expr>,
pub offset: Option<Expr>,
pub offset_before_limit: bool,
pub fields: Vec<ProjectionField>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OrderClause {
pub keys: Vec<OrderKey>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OrderKey {
pub expr: Expr,
pub descending: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InsertExpr {
pub target: String,
pub rows: Vec<Vec<Assignment>>,
pub returning: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct UpdateExpr {
pub source: String,
pub alias: Option<String>,
pub filter: Option<Expr>,
pub assignments: Vec<Assignment>,
pub returning: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DeleteExpr {
pub source: String,
pub alias: Option<String>,
pub filter: Option<Expr>,
pub returning: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Assignment {
pub field: String,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq)]
pub struct UpsertExpr {
pub target: String,
pub key_column: String,
pub assignments: Vec<Assignment>,
pub on_conflict: Vec<Assignment>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CreateTypeExpr {
pub name: String,
pub fields: Vec<FieldDef>,
pub if_not_exists: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FieldDef {
pub name: String,
pub type_name: String,
pub required: bool,
pub unique: bool,
pub default: Option<Literal>,
pub auto: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AggregateExpr {
pub function: AggFunc,
pub argument: Option<Expr>,
pub mode: AggregateMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AggregateMode {
Symmetric,
Raw,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AggFunc {
Count,
CountDistinct,
Avg,
Sum,
Min,
Max,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowFunc {
RowNumber,
Rank,
DenseRank,
Sum,
Avg,
Count,
Min,
Max,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScalarFn {
Upper,
Lower,
Length,
Trim,
Substring, Concat, Abs,
Round, Ceil,
Floor,
Sqrt,
Pow, Now, Extract, DateAdd, DateDiff, JsonType, JsonText, }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CastType {
Int,
Float,
Str,
Bool,
DateTime,
Uuid,
Bytes,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PathSeg {
Key(String),
Index(u32),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Field(String),
QualifiedField {
qualifier: String,
field: String,
},
Literal(Literal),
Param(String),
BinaryOp(Box<Expr>, BinOp, Box<Expr>),
UnaryOp(UnaryOp, Box<Expr>),
FunctionCall(AggFunc, Box<Expr>, AggregateMode),
ScalarFunc(ScalarFn, Vec<Expr>),
Coalesce(Box<Expr>, Box<Expr>),
InList {
expr: Box<Expr>,
list: Vec<Expr>,
negated: bool,
},
InSubquery {
expr: Box<Expr>,
subquery: Box<QueryExpr>,
negated: bool,
},
ExistsSubquery {
subquery: Box<QueryExpr>,
negated: bool,
},
Case {
whens: Vec<(Box<Expr>, Box<Expr>)>,
else_expr: Option<Box<Expr>>,
},
Window {
function: WindowFunc,
args: Vec<Expr>,
mode: AggregateMode,
partition_by: Vec<Expr>,
order_by: Vec<OrderKey>,
},
Cast(Box<Expr>, CastType),
ValueLit(Value),
Null,
NestedQuery(Box<NestedQuery>),
LinkPath {
outer_alias: String,
links: Vec<String>,
column: String,
},
JsonPath {
base: Box<Expr>,
segments: Vec<PathSeg>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct JsonPathIdentityV1 {
pub root: JsonPathRootV1,
pub segments: Vec<PathSeg>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum JsonPathRootV1 {
Unqualified(String),
Qualified { qualifier: String, field: String },
}
impl JsonPathIdentityV1 {
pub const VERSION: u8 = 1;
pub fn from_expr(expr: &Expr) -> Option<Self> {
let Expr::JsonPath { base, segments } = expr else {
return None;
};
let root = match base.as_ref() {
Expr::Field(field) => JsonPathRootV1::Unqualified(field.clone()),
Expr::QualifiedField { qualifier, field } => JsonPathRootV1::Qualified {
qualifier: qualifier.clone(),
field: field.clone(),
},
_ => return None,
};
Some(Self {
root,
segments: segments.clone(),
})
}
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut out = vec![Self::VERSION];
match &self.root {
JsonPathRootV1::Unqualified(field) => {
out.push(1);
push_identity_str(&mut out, field);
}
JsonPathRootV1::Qualified { qualifier, field } => {
out.push(2);
push_identity_str(&mut out, qualifier);
push_identity_str(&mut out, field);
}
}
out.extend_from_slice(&(self.segments.len() as u32).to_le_bytes());
for segment in &self.segments {
match segment {
PathSeg::Key(key) => {
out.push(1);
push_identity_str(&mut out, key);
}
PathSeg::Index(index) => {
out.push(2);
out.extend_from_slice(&index.to_le_bytes());
}
}
}
out
}
pub fn canonical_text(&self) -> String {
let mut out = match &self.root {
JsonPathRootV1::Unqualified(field) => format!("v1:.{field}"),
JsonPathRootV1::Qualified { qualifier, field } => {
format!("v1:{qualifier}.{field}")
}
};
for segment in &self.segments {
match segment {
PathSeg::Key(key) => {
out.push_str("->\"");
push_identity_escaped(&mut out, key);
out.push('"');
}
PathSeg::Index(index) => {
out.push_str("->");
out.push_str(&index.to_string());
}
}
}
out
}
pub fn bind_table_local(
&self,
qualifier: Option<&str>,
) -> Option<powdb_storage::stored_json_path::StoredJsonPathV1> {
use powdb_storage::stored_json_path::{StoredJsonPathSegmentV1, StoredJsonPathV1};
let column = match &self.root {
JsonPathRootV1::Unqualified(field) => field.clone(),
JsonPathRootV1::Qualified {
qualifier: actual,
field,
} if qualifier == Some(actual.as_str()) => field.clone(),
JsonPathRootV1::Qualified { .. } => return None,
};
Some(StoredJsonPathV1::new(
column,
self.segments
.iter()
.map(|segment| match segment {
PathSeg::Key(key) => StoredJsonPathSegmentV1::Key(key.clone()),
PathSeg::Index(index) => StoredJsonPathSegmentV1::Index(*index),
})
.collect(),
))
}
}
pub fn projection_output_name(expr: &Expr) -> String {
match expr {
Expr::Field(name) => name.clone(),
Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
other => render_expr(other).unwrap_or_else(|| "?".into()),
}
}
fn render_expr(expr: &Expr) -> Option<String> {
Some(match expr {
Expr::Field(name) => format!(".{name}"),
Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
Expr::Literal(literal) => render_literal(literal),
Expr::Null => "null".into(),
Expr::Param(name) => format!("${name}"),
Expr::BinaryOp(left, op, right) => format!(
"{} {} {}",
render_expr(left)?,
render_binop(*op),
render_expr(right)?
),
Expr::UnaryOp(op, inner) => match op {
UnaryOp::Not => format!("not {}", render_expr(inner)?),
UnaryOp::Exists => format!("exists {}", render_expr(inner)?),
UnaryOp::NotExists => format!("not exists {}", render_expr(inner)?),
UnaryOp::IsNull => format!("{} is null", render_expr(inner)?),
UnaryOp::IsNotNull => format!("{} is not null", render_expr(inner)?),
},
Expr::FunctionCall(func, argument, _) => match func {
AggFunc::CountDistinct => format!("count(distinct {})", render_expr(argument)?),
_ => format!("{}({})", render_agg(*func), render_expr(argument)?),
},
Expr::ScalarFunc(func, args) => {
let mut rendered = Vec::with_capacity(args.len());
for arg in args {
rendered.push(render_expr(arg)?);
}
format!("{}({})", render_scalar_fn(*func), rendered.join(", "))
}
Expr::Coalesce(left, right) => {
format!("coalesce({}, {})", render_expr(left)?, render_expr(right)?)
}
Expr::Cast(inner, target) => format!("{}({})", render_cast(*target), render_expr(inner)?),
Expr::JsonPath { base, segments } => {
let mut out = render_expr(base)?;
for segment in segments {
out.push_str("->");
match segment {
PathSeg::Key(key) => out.push_str(key),
PathSeg::Index(index) => out.push_str(&index.to_string()),
}
}
out
}
_ => return None,
})
}
fn render_literal(literal: &Literal) -> String {
match literal {
Literal::Int(v) => v.to_string(),
Literal::Float(v) => v.to_string(),
Literal::Bool(v) => v.to_string(),
Literal::String(s) => format!("\"{s}\""),
}
}
fn render_binop(op: BinOp) -> &'static str {
match op {
BinOp::Eq => "=",
BinOp::Neq => "!=",
BinOp::Lt => "<",
BinOp::Gt => ">",
BinOp::Lte => "<=",
BinOp::Gte => ">=",
BinOp::And => "and",
BinOp::Or => "or",
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::Div => "/",
BinOp::Like => "like",
}
}
fn render_agg(func: AggFunc) -> &'static str {
match func {
AggFunc::Count | AggFunc::CountDistinct => "count",
AggFunc::Avg => "avg",
AggFunc::Sum => "sum",
AggFunc::Min => "min",
AggFunc::Max => "max",
}
}
fn render_scalar_fn(func: ScalarFn) -> &'static str {
match func {
ScalarFn::Upper => "upper",
ScalarFn::Lower => "lower",
ScalarFn::Length => "length",
ScalarFn::Trim => "trim",
ScalarFn::Substring => "substring",
ScalarFn::Concat => "concat",
ScalarFn::Abs => "abs",
ScalarFn::Round => "round",
ScalarFn::Ceil => "ceil",
ScalarFn::Floor => "floor",
ScalarFn::Sqrt => "sqrt",
ScalarFn::Pow => "pow",
ScalarFn::Now => "now",
ScalarFn::Extract => "extract",
ScalarFn::DateAdd => "date_add",
ScalarFn::DateDiff => "date_diff",
ScalarFn::JsonType => "json_type",
ScalarFn::JsonText => "json_text",
}
}
fn render_cast(target: CastType) -> &'static str {
match target {
CastType::Int => "int",
CastType::Float => "float",
CastType::Str => "str",
CastType::Bool => "bool",
CastType::DateTime => "datetime",
CastType::Uuid => "uuid",
CastType::Bytes => "bytes",
}
}
pub fn expression_output_name(expr: &Expr) -> String {
match expr {
Expr::Field(field) => field.clone(),
Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
Expr::JsonPath { .. } => JsonPathIdentityV1::from_expr(expr)
.map(|path| path.canonical_text())
.unwrap_or_else(|| "?".into()),
_ => "?".into(),
}
}
fn push_identity_str(out: &mut Vec<u8>, value: &str) {
out.extend_from_slice(&(value.len() as u32).to_le_bytes());
out.extend_from_slice(value.as_bytes());
}
fn push_identity_escaped(out: &mut String, value: &str) {
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c <= '\u{1f}' => {
use std::fmt::Write;
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
Int(i64),
Float(f64),
String(String),
Bool(bool),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParamValue {
Null,
Int(i64),
Float(f64),
Bool(bool),
Str(String),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BinOp {
Eq,
Neq,
Lt,
Gt,
Lte,
Gte,
And,
Or,
Add,
Sub,
Mul,
Div,
Like,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UnaryOp {
Not,
Exists,
NotExists,
IsNull,
IsNotNull,
}
#[cfg(test)]
mod json_path_identity_tests {
use super::*;
#[test]
fn canonical_path_identity_goldens() {
let unquoted = JsonPathIdentityV1 {
root: JsonPathRootV1::Unqualified("data".into()),
segments: vec![PathSeg::Key("author".into())],
};
let quoted = JsonPathIdentityV1 {
root: JsonPathRootV1::Unqualified("data".into()),
segments: vec![PathSeg::Key("author".into())],
};
assert_eq!(unquoted, quoted);
assert_eq!(unquoted.canonical_text(), "v1:.data->\"author\"");
assert_eq!(
unquoted.canonical_bytes(),
vec![
1, 1, 4, 0, 0, 0, b'd', b'a', b't', b'a', 1, 0, 0, 0, 1, 6, 0, 0, 0, b'a', b'u',
b't', b'h', b'o', b'r',
]
);
let key_zero = JsonPathIdentityV1 {
root: JsonPathRootV1::Unqualified("data".into()),
segments: vec![PathSeg::Key("0".into())],
};
let index_zero = JsonPathIdentityV1 {
root: JsonPathRootV1::Unqualified("data".into()),
segments: vec![PathSeg::Index(0)],
};
assert_ne!(key_zero.canonical_bytes(), index_zero.canonical_bytes());
let unicode = JsonPathIdentityV1 {
root: JsonPathRootV1::Unqualified("data".into()),
segments: vec![PathSeg::Key("café\n\"x\\y".into())],
};
assert_eq!(unicode.canonical_text(), "v1:.data->\"café\\n\\\"x\\\\y\"");
}
#[test]
fn qualified_root_binding_is_explicit() {
let qualified = JsonPathIdentityV1 {
root: JsonPathRootV1::Qualified {
qualifier: "p".into(),
field: "data".into(),
},
segments: vec![PathSeg::Key("age".into())],
};
assert!(qualified.bind_table_local(Some("other")).is_none());
let stored = qualified
.bind_table_local(Some("p"))
.expect("matching root");
assert_eq!(stored.canonical_text(), "v1:.data->\"age\"");
}
}