1use powdb_storage::types::Value;
2
3#[derive(Debug, Clone, PartialEq)]
5pub enum Statement {
6 Query(QueryExpr),
7 Insert(InsertExpr),
8 UpdateQuery(UpdateExpr),
9 DeleteQuery(DeleteExpr),
10 CreateType(CreateTypeExpr),
11 CreateLink(CreateLinkExpr),
14 DropLink(DropLinkExpr),
17 AlterTable(AlterTableExpr),
18 DropTable(DropTableExpr),
19 CreateView(CreateViewExpr),
20 RefreshView(RefreshViewExpr),
21 DropView(DropViewExpr),
22 Union(UnionExpr),
23 Upsert(UpsertExpr),
24 Explain(Box<Statement>),
25 Begin,
26 Commit,
27 Rollback,
28 ListTypes,
30 Describe(String),
33 ListLinks,
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct AlterTableExpr {
40 pub table: String,
41 pub action: AlterAction,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct CreateLinkExpr {
53 pub owner: String,
55 pub name: String,
57 pub target: String,
59 pub local_key: String,
61 pub target_key: String,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct DropLinkExpr {
69 pub owner: String,
71 pub name: String,
73 pub if_exists: bool,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ViaLink {
85 pub outer_alias: String,
87 pub link_name: String,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94pub enum IndexTarget {
95 Column(String),
96 JsonPath(powdb_storage::stored_json_path::StoredJsonPathV1),
97}
98
99#[derive(Debug, Clone, PartialEq)]
101pub enum AlterAction {
102 AddColumn {
103 name: String,
104 type_name: String,
105 required: bool,
106 },
107 DropColumn {
108 name: String,
109 if_exists: bool,
112 },
113 AddIndex {
116 target: IndexTarget,
117 if_not_exists: bool,
120 },
121 AddUnique {
127 target: IndexTarget,
128 if_not_exists: bool,
129 },
130 DropIndex {
133 target: IndexTarget,
134 if_exists: bool,
135 },
136 AddLink {
140 name: String,
141 target: String,
142 local_key: String,
143 target_key: String,
144 },
145}
146
147#[derive(Debug, Clone, PartialEq)]
149pub struct DropTableExpr {
150 pub table: String,
151 pub if_exists: bool,
153}
154
155#[derive(Debug, Clone, PartialEq)]
157pub struct CreateViewExpr {
158 pub name: String,
159 pub query: QueryExpr,
160 pub query_text: String,
162}
163
164#[derive(Debug, Clone, PartialEq)]
166pub struct RefreshViewExpr {
167 pub name: String,
168}
169
170#[derive(Debug, Clone, PartialEq)]
172pub struct DropViewExpr {
173 pub name: String,
174 pub if_exists: bool,
176}
177
178#[derive(Debug, Clone, PartialEq)]
180pub struct UnionExpr {
181 pub left: Box<Statement>,
182 pub right: Box<Statement>,
183 pub all: bool,
185}
186
187#[derive(Debug, Clone, PartialEq)]
189pub struct QueryExpr {
190 pub source: String,
191 pub alias: Option<String>,
195 pub joins: Vec<JoinClause>,
199 pub filter: Option<Expr>,
200 pub order: Option<OrderClause>,
201 pub limit: Option<Expr>,
202 pub offset: Option<Expr>,
203 pub projection: Option<Vec<ProjectionField>>,
204 pub aggregation: Option<AggregateExpr>,
205 pub distinct: bool,
206 pub group_by: Option<GroupByClause>,
207}
208
209#[derive(Debug, Clone, PartialEq)]
211pub struct GroupByClause {
212 pub keys: Vec<GroupKey>,
213 pub having: Option<Expr>,
214}
215
216#[derive(Debug, Clone, PartialEq)]
218pub struct GroupKey {
219 pub expr: Expr,
220 pub output_name: String,
221}
222
223impl GroupKey {
224 pub fn output_name(&self) -> String {
228 self.output_name.clone()
229 }
230}
231
232#[derive(Debug, Clone, PartialEq)]
237pub struct JoinClause {
238 pub kind: JoinKind,
239 pub source: String,
240 pub alias: Option<String>,
241 pub on: Option<Expr>,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum JoinKind {
247 Inner,
248 LeftOuter,
249 RightOuter,
250 Cross,
251}
252
253#[derive(Debug, Clone, PartialEq)]
254pub struct ProjectionField {
255 pub alias: Option<String>,
256 pub expr: Expr,
257}
258
259#[derive(Debug, Clone, PartialEq)]
265pub struct NestedQuery {
266 pub source: String,
267 pub alias: String,
268 pub via_link: Option<ViaLink>,
274 pub filter: Expr,
275 pub order: Option<OrderClause>,
277 pub limit: Option<Expr>,
279 pub offset: Option<Expr>,
280 pub offset_before_limit: bool,
284 pub fields: Vec<ProjectionField>,
285}
286
287#[derive(Debug, Clone, PartialEq)]
288pub struct OrderClause {
289 pub keys: Vec<OrderKey>,
290}
291
292#[derive(Debug, Clone, PartialEq)]
293pub struct OrderKey {
294 pub expr: Expr,
295 pub descending: bool,
296}
297
298#[derive(Debug, Clone, PartialEq)]
299pub struct InsertExpr {
300 pub target: String,
301 pub rows: Vec<Vec<Assignment>>,
304 pub returning: bool,
307}
308
309#[derive(Debug, Clone, PartialEq)]
310pub struct UpdateExpr {
311 pub source: String,
312 pub alias: Option<String>,
315 pub filter: Option<Expr>,
316 pub assignments: Vec<Assignment>,
317 pub returning: bool,
320}
321
322#[derive(Debug, Clone, PartialEq)]
323pub struct DeleteExpr {
324 pub source: String,
325 pub alias: Option<String>,
328 pub filter: Option<Expr>,
329 pub returning: bool,
332}
333
334#[derive(Debug, Clone, PartialEq)]
335pub struct Assignment {
336 pub field: String,
337 pub value: Expr,
338}
339
340#[derive(Debug, Clone, PartialEq)]
341pub struct UpsertExpr {
343 pub target: String,
344 pub key_column: String,
345 pub assignments: Vec<Assignment>,
346 pub on_conflict: Vec<Assignment>,
349}
350
351#[derive(Debug, Clone, PartialEq)]
352pub struct CreateTypeExpr {
353 pub name: String,
354 pub fields: Vec<FieldDef>,
355 pub if_not_exists: bool,
358}
359
360#[derive(Debug, Clone, PartialEq)]
361pub struct FieldDef {
362 pub name: String,
363 pub type_name: String,
364 pub required: bool,
365 pub unique: bool,
368 pub default: Option<Literal>,
371 pub auto: bool,
374}
375
376#[derive(Debug, Clone, PartialEq)]
377pub struct AggregateExpr {
378 pub function: AggFunc,
379 pub argument: Option<Expr>,
380 pub mode: AggregateMode,
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
384pub enum AggregateMode {
385 Symmetric,
386 Raw,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq)]
390pub enum AggFunc {
391 Count,
392 CountDistinct,
393 Avg,
394 Sum,
395 Min,
396 Max,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq)]
401pub enum WindowFunc {
402 RowNumber,
403 Rank,
404 DenseRank,
405 Sum,
406 Avg,
407 Count,
408 Min,
409 Max,
410}
411
412#[derive(Debug, Clone, Copy, PartialEq)]
414pub enum ScalarFn {
415 Upper,
416 Lower,
417 Length,
418 Trim,
419 Substring, Concat, Abs,
423 Round, Ceil,
425 Floor,
426 Sqrt,
427 Pow, Now, Extract, DateAdd, DateDiff, JsonType, JsonText, }
437
438#[derive(Debug, Clone, Copy, PartialEq)]
440pub enum CastType {
441 Int,
442 Float,
443 Str,
444 Bool,
445 DateTime,
446 Uuid,
447 Bytes,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Hash)]
458pub enum PathSeg {
459 Key(String),
461 Index(u32),
463}
464
465#[derive(Debug, Clone, PartialEq)]
467pub enum Expr {
468 Field(String),
469 QualifiedField {
474 qualifier: String,
475 field: String,
476 },
477 Literal(Literal),
478 Param(String),
479 BinaryOp(Box<Expr>, BinOp, Box<Expr>),
480 UnaryOp(UnaryOp, Box<Expr>),
481 FunctionCall(AggFunc, Box<Expr>, AggregateMode),
482 ScalarFunc(ScalarFn, Vec<Expr>),
484 Coalesce(Box<Expr>, Box<Expr>),
485 InList {
487 expr: Box<Expr>,
488 list: Vec<Expr>,
489 negated: bool,
490 },
491 InSubquery {
494 expr: Box<Expr>,
495 subquery: Box<QueryExpr>,
496 negated: bool,
497 },
498 ExistsSubquery {
502 subquery: Box<QueryExpr>,
503 negated: bool,
504 },
505 Case {
507 whens: Vec<(Box<Expr>, Box<Expr>)>,
508 else_expr: Option<Box<Expr>>,
509 },
510 Window {
512 function: WindowFunc,
513 args: Vec<Expr>,
514 mode: AggregateMode,
515 partition_by: Vec<Expr>,
516 order_by: Vec<OrderKey>,
517 },
518 Cast(Box<Expr>, CastType),
520 ValueLit(Value),
525 Null,
527 NestedQuery(Box<NestedQuery>),
531 LinkPath {
536 outer_alias: String,
538 links: Vec<String>,
541 column: String,
543 },
544 JsonPath {
549 base: Box<Expr>,
550 segments: Vec<PathSeg>,
551 },
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Hash)]
557pub struct JsonPathIdentityV1 {
558 pub root: JsonPathRootV1,
559 pub segments: Vec<PathSeg>,
560}
561
562#[derive(Debug, Clone, PartialEq, Eq, Hash)]
563pub enum JsonPathRootV1 {
564 Unqualified(String),
565 Qualified { qualifier: String, field: String },
566}
567
568impl JsonPathIdentityV1 {
569 pub const VERSION: u8 = 1;
570
571 pub fn from_expr(expr: &Expr) -> Option<Self> {
572 let Expr::JsonPath { base, segments } = expr else {
573 return None;
574 };
575 let root = match base.as_ref() {
576 Expr::Field(field) => JsonPathRootV1::Unqualified(field.clone()),
577 Expr::QualifiedField { qualifier, field } => JsonPathRootV1::Qualified {
578 qualifier: qualifier.clone(),
579 field: field.clone(),
580 },
581 _ => return None,
582 };
583 Some(Self {
584 root,
585 segments: segments.clone(),
586 })
587 }
588
589 pub fn canonical_bytes(&self) -> Vec<u8> {
590 let mut out = vec![Self::VERSION];
591 match &self.root {
592 JsonPathRootV1::Unqualified(field) => {
593 out.push(1);
594 push_identity_str(&mut out, field);
595 }
596 JsonPathRootV1::Qualified { qualifier, field } => {
597 out.push(2);
598 push_identity_str(&mut out, qualifier);
599 push_identity_str(&mut out, field);
600 }
601 }
602 out.extend_from_slice(&(self.segments.len() as u32).to_le_bytes());
603 for segment in &self.segments {
604 match segment {
605 PathSeg::Key(key) => {
606 out.push(1);
607 push_identity_str(&mut out, key);
608 }
609 PathSeg::Index(index) => {
610 out.push(2);
611 out.extend_from_slice(&index.to_le_bytes());
612 }
613 }
614 }
615 out
616 }
617
618 pub fn canonical_text(&self) -> String {
619 let mut out = match &self.root {
620 JsonPathRootV1::Unqualified(field) => format!("v1:.{field}"),
621 JsonPathRootV1::Qualified { qualifier, field } => {
622 format!("v1:{qualifier}.{field}")
623 }
624 };
625 for segment in &self.segments {
626 match segment {
627 PathSeg::Key(key) => {
628 out.push_str("->\"");
629 push_identity_escaped(&mut out, key);
630 out.push('"');
631 }
632 PathSeg::Index(index) => {
633 out.push_str("->");
634 out.push_str(&index.to_string());
635 }
636 }
637 }
638 out
639 }
640
641 pub fn bind_table_local(
645 &self,
646 qualifier: Option<&str>,
647 ) -> Option<powdb_storage::stored_json_path::StoredJsonPathV1> {
648 use powdb_storage::stored_json_path::{StoredJsonPathSegmentV1, StoredJsonPathV1};
649 let column = match &self.root {
650 JsonPathRootV1::Unqualified(field) => field.clone(),
651 JsonPathRootV1::Qualified {
652 qualifier: actual,
653 field,
654 } if qualifier == Some(actual.as_str()) => field.clone(),
655 JsonPathRootV1::Qualified { .. } => return None,
656 };
657 Some(StoredJsonPathV1::new(
658 column,
659 self.segments
660 .iter()
661 .map(|segment| match segment {
662 PathSeg::Key(key) => StoredJsonPathSegmentV1::Key(key.clone()),
663 PathSeg::Index(index) => StoredJsonPathSegmentV1::Index(*index),
664 })
665 .collect(),
666 ))
667 }
668}
669
670pub fn projection_output_name(expr: &Expr) -> String {
680 match expr {
681 Expr::Field(name) => name.clone(),
684 Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
685 other => render_expr(other).unwrap_or_else(|| "?".into()),
686 }
687}
688
689fn render_expr(expr: &Expr) -> Option<String> {
690 Some(match expr {
691 Expr::Field(name) => format!(".{name}"),
692 Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
693 Expr::Literal(literal) => render_literal(literal),
694 Expr::Null => "null".into(),
695 Expr::Param(name) => format!("${name}"),
696 Expr::BinaryOp(left, op, right) => format!(
697 "{} {} {}",
698 render_expr(left)?,
699 render_binop(*op),
700 render_expr(right)?
701 ),
702 Expr::UnaryOp(op, inner) => match op {
703 UnaryOp::Not => format!("not {}", render_expr(inner)?),
704 UnaryOp::Exists => format!("exists {}", render_expr(inner)?),
705 UnaryOp::NotExists => format!("not exists {}", render_expr(inner)?),
706 UnaryOp::IsNull => format!("{} is null", render_expr(inner)?),
707 UnaryOp::IsNotNull => format!("{} is not null", render_expr(inner)?),
708 },
709 Expr::FunctionCall(func, argument, _) => match func {
710 AggFunc::CountDistinct => format!("count(distinct {})", render_expr(argument)?),
711 _ => format!("{}({})", render_agg(*func), render_expr(argument)?),
712 },
713 Expr::ScalarFunc(func, args) => {
714 let mut rendered = Vec::with_capacity(args.len());
715 for arg in args {
716 rendered.push(render_expr(arg)?);
717 }
718 format!("{}({})", render_scalar_fn(*func), rendered.join(", "))
719 }
720 Expr::Coalesce(left, right) => {
721 format!("coalesce({}, {})", render_expr(left)?, render_expr(right)?)
722 }
723 Expr::Cast(inner, target) => format!("{}({})", render_cast(*target), render_expr(inner)?),
724 Expr::JsonPath { base, segments } => {
725 let mut out = render_expr(base)?;
726 for segment in segments {
727 out.push_str("->");
728 match segment {
729 PathSeg::Key(key) => out.push_str(key),
730 PathSeg::Index(index) => out.push_str(&index.to_string()),
731 }
732 }
733 out
734 }
735 _ => return None,
736 })
737}
738
739fn render_literal(literal: &Literal) -> String {
740 match literal {
741 Literal::Int(v) => v.to_string(),
742 Literal::Float(v) => v.to_string(),
743 Literal::Bool(v) => v.to_string(),
744 Literal::String(s) => format!("\"{s}\""),
745 }
746}
747
748fn render_binop(op: BinOp) -> &'static str {
749 match op {
750 BinOp::Eq => "=",
751 BinOp::Neq => "!=",
752 BinOp::Lt => "<",
753 BinOp::Gt => ">",
754 BinOp::Lte => "<=",
755 BinOp::Gte => ">=",
756 BinOp::And => "and",
757 BinOp::Or => "or",
758 BinOp::Add => "+",
759 BinOp::Sub => "-",
760 BinOp::Mul => "*",
761 BinOp::Div => "/",
762 BinOp::Like => "like",
763 }
764}
765
766fn render_agg(func: AggFunc) -> &'static str {
767 match func {
768 AggFunc::Count | AggFunc::CountDistinct => "count",
769 AggFunc::Avg => "avg",
770 AggFunc::Sum => "sum",
771 AggFunc::Min => "min",
772 AggFunc::Max => "max",
773 }
774}
775
776fn render_scalar_fn(func: ScalarFn) -> &'static str {
777 match func {
778 ScalarFn::Upper => "upper",
779 ScalarFn::Lower => "lower",
780 ScalarFn::Length => "length",
781 ScalarFn::Trim => "trim",
782 ScalarFn::Substring => "substring",
783 ScalarFn::Concat => "concat",
784 ScalarFn::Abs => "abs",
785 ScalarFn::Round => "round",
786 ScalarFn::Ceil => "ceil",
787 ScalarFn::Floor => "floor",
788 ScalarFn::Sqrt => "sqrt",
789 ScalarFn::Pow => "pow",
790 ScalarFn::Now => "now",
791 ScalarFn::Extract => "extract",
792 ScalarFn::DateAdd => "date_add",
793 ScalarFn::DateDiff => "date_diff",
794 ScalarFn::JsonType => "json_type",
795 ScalarFn::JsonText => "json_text",
796 }
797}
798
799fn render_cast(target: CastType) -> &'static str {
800 match target {
801 CastType::Int => "int",
802 CastType::Float => "float",
803 CastType::Str => "str",
804 CastType::Bool => "bool",
805 CastType::DateTime => "datetime",
806 CastType::Uuid => "uuid",
807 CastType::Bytes => "bytes",
808 }
809}
810
811pub fn expression_output_name(expr: &Expr) -> String {
812 match expr {
813 Expr::Field(field) => field.clone(),
814 Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
815 Expr::JsonPath { .. } => JsonPathIdentityV1::from_expr(expr)
816 .map(|path| path.canonical_text())
817 .unwrap_or_else(|| "?".into()),
818 _ => "?".into(),
819 }
820}
821
822fn push_identity_str(out: &mut Vec<u8>, value: &str) {
823 out.extend_from_slice(&(value.len() as u32).to_le_bytes());
824 out.extend_from_slice(value.as_bytes());
825}
826
827fn push_identity_escaped(out: &mut String, value: &str) {
828 for ch in value.chars() {
829 match ch {
830 '"' => out.push_str("\\\""),
831 '\\' => out.push_str("\\\\"),
832 '\n' => out.push_str("\\n"),
833 '\r' => out.push_str("\\r"),
834 '\t' => out.push_str("\\t"),
835 c if c <= '\u{1f}' => {
836 use std::fmt::Write;
837 let _ = write!(out, "\\u{:04x}", c as u32);
838 }
839 c => out.push(c),
840 }
841 }
842}
843
844#[derive(Debug, Clone, PartialEq)]
845pub enum Literal {
846 Int(i64),
847 Float(f64),
848 String(String),
849 Bool(bool),
850}
851
852#[derive(Debug, Clone, PartialEq)]
860pub enum ParamValue {
861 Null,
862 Int(i64),
863 Float(f64),
864 Bool(bool),
865 Str(String),
866}
867
868#[derive(Debug, Clone, Copy, PartialEq)]
869pub enum BinOp {
870 Eq,
871 Neq,
872 Lt,
873 Gt,
874 Lte,
875 Gte,
876 And,
877 Or,
878 Add,
879 Sub,
880 Mul,
881 Div,
882 Like,
883}
884
885#[derive(Debug, Clone, Copy, PartialEq)]
886pub enum UnaryOp {
887 Not,
888 Exists,
889 NotExists,
890 IsNull,
891 IsNotNull,
892}
893
894#[cfg(test)]
895mod json_path_identity_tests {
896 use super::*;
897
898 #[test]
899 fn canonical_path_identity_goldens() {
900 let unquoted = JsonPathIdentityV1 {
901 root: JsonPathRootV1::Unqualified("data".into()),
902 segments: vec![PathSeg::Key("author".into())],
903 };
904 let quoted = JsonPathIdentityV1 {
905 root: JsonPathRootV1::Unqualified("data".into()),
906 segments: vec![PathSeg::Key("author".into())],
907 };
908 assert_eq!(unquoted, quoted);
909 assert_eq!(unquoted.canonical_text(), "v1:.data->\"author\"");
910 assert_eq!(
911 unquoted.canonical_bytes(),
912 vec![
913 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',
914 b't', b'h', b'o', b'r',
915 ]
916 );
917
918 let key_zero = JsonPathIdentityV1 {
919 root: JsonPathRootV1::Unqualified("data".into()),
920 segments: vec![PathSeg::Key("0".into())],
921 };
922 let index_zero = JsonPathIdentityV1 {
923 root: JsonPathRootV1::Unqualified("data".into()),
924 segments: vec![PathSeg::Index(0)],
925 };
926 assert_ne!(key_zero.canonical_bytes(), index_zero.canonical_bytes());
927
928 let unicode = JsonPathIdentityV1 {
929 root: JsonPathRootV1::Unqualified("data".into()),
930 segments: vec![PathSeg::Key("café\n\"x\\y".into())],
931 };
932 assert_eq!(unicode.canonical_text(), "v1:.data->\"café\\n\\\"x\\\\y\"");
933 }
934
935 #[test]
936 fn qualified_root_binding_is_explicit() {
937 let qualified = JsonPathIdentityV1 {
938 root: JsonPathRootV1::Qualified {
939 qualifier: "p".into(),
940 field: "data".into(),
941 },
942 segments: vec![PathSeg::Key("age".into())],
943 };
944 assert!(qualified.bind_table_local(Some("other")).is_none());
945 let stored = qualified
946 .bind_table_local(Some("p"))
947 .expect("matching root");
948 assert_eq!(stored.canonical_text(), "v1:.data->\"age\"");
949 }
950}