use powdb_storage::types::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum Statement {
Query(QueryExpr),
Insert(InsertExpr),
UpdateQuery(UpdateExpr),
DeleteQuery(DeleteExpr),
CreateType(CreateTypeExpr),
AlterTable(AlterTableExpr),
DropTable(DropTableExpr),
CreateView(CreateViewExpr),
RefreshView(RefreshViewExpr),
DropView(DropViewExpr),
Union(UnionExpr),
Upsert(UpsertExpr),
Explain(Box<Statement>),
Begin,
Commit,
Rollback,
ListTypes,
Describe(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlterTableExpr {
pub table: String,
pub action: AlterAction,
}
#[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,
},
}
#[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 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 filter: Option<Expr>,
pub assignments: Vec<Assignment>,
pub returning: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DeleteExpr {
pub source: 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,
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 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\"");
}
}