1use powdb_storage::types::Value;
2
3pub(crate) const OUTER_CORRELATION_QUALIFIER: &str = "\0powdb_outer_correlated_reference";
4
5pub(crate) fn outer_correlation_qualifier(visible: &str) -> String {
6 format!("{OUTER_CORRELATION_QUALIFIER}{visible}")
7}
8
9pub(crate) fn outer_correlation_alias(qualifier: &str) -> Option<&str> {
10 qualifier
11 .strip_prefix(OUTER_CORRELATION_QUALIFIER)
12 .filter(|alias| !alias.is_empty())
13}
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum Statement {
18 Query(QueryExpr),
19 Insert(InsertExpr),
20 UpdateQuery(UpdateExpr),
21 DeleteQuery(DeleteExpr),
22 CreateType(CreateTypeExpr),
23 CreateLink(CreateLinkExpr),
26 DropLink(DropLinkExpr),
29 AlterTable(AlterTableExpr),
30 DropTable(DropTableExpr),
31 CreateView(CreateViewExpr),
32 RefreshView(RefreshViewExpr),
33 DropView(DropViewExpr),
34 Union(UnionExpr),
35 Upsert(UpsertExpr),
36 Explain(Box<Statement>),
37 Begin,
38 Commit,
39 Rollback,
40 ListTypes,
42 Describe(String),
45 ListLinks,
47}
48
49#[derive(Debug, Clone, PartialEq)]
51pub struct AlterTableExpr {
52 pub table: String,
53 pub action: AlterAction,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct CreateLinkExpr {
65 pub owner: String,
67 pub name: String,
69 pub target: String,
71 pub local_key: String,
73 pub target_key: String,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct DropLinkExpr {
81 pub owner: String,
83 pub name: String,
85 pub if_exists: bool,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct ViaLink {
97 pub outer_alias: String,
99 pub link_name: String,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Hash)]
106pub enum IndexTarget {
107 Column(String),
108 JsonPath(powdb_storage::stored_json_path::StoredJsonPathV1),
109}
110
111#[derive(Debug, Clone, PartialEq)]
113pub enum AlterAction {
114 AddColumn {
115 name: String,
116 type_name: String,
117 required: bool,
118 },
119 DropColumn {
120 name: String,
121 if_exists: bool,
124 },
125 AddIndex {
128 target: IndexTarget,
129 if_not_exists: bool,
132 },
133 AddUnique {
139 target: IndexTarget,
140 if_not_exists: bool,
141 },
142 DropIndex {
145 target: IndexTarget,
146 if_exists: bool,
147 },
148 AddLink {
152 name: String,
153 target: String,
154 local_key: String,
155 target_key: String,
156 },
157}
158
159#[derive(Debug, Clone, PartialEq)]
161pub struct DropTableExpr {
162 pub table: String,
163 pub if_exists: bool,
165}
166
167#[derive(Debug, Clone, PartialEq)]
169pub struct CreateViewExpr {
170 pub name: String,
171 pub query: QueryExpr,
172 pub query_text: String,
174}
175
176#[derive(Debug, Clone, PartialEq)]
178pub struct RefreshViewExpr {
179 pub name: String,
180}
181
182#[derive(Debug, Clone, PartialEq)]
184pub struct DropViewExpr {
185 pub name: String,
186 pub if_exists: bool,
188}
189
190#[derive(Debug, Clone, PartialEq)]
192pub struct UnionExpr {
193 pub left: Box<Statement>,
194 pub right: Box<Statement>,
195 pub all: bool,
197}
198
199#[derive(Debug, Clone, PartialEq)]
201pub struct QueryExpr {
202 pub source: String,
203 pub alias: Option<String>,
207 pub joins: Vec<JoinClause>,
211 pub filter: Option<Expr>,
212 pub order: Option<OrderClause>,
213 pub limit: Option<Expr>,
214 pub offset: Option<Expr>,
215 pub projection: Option<Vec<ProjectionField>>,
216 pub aggregation: Option<AggregateExpr>,
217 pub distinct: bool,
218 pub group_by: Option<GroupByClause>,
219}
220
221#[derive(Debug, Clone, PartialEq)]
223pub struct GroupByClause {
224 pub keys: Vec<GroupKey>,
225 pub having: Option<Expr>,
226}
227
228#[derive(Debug, Clone, PartialEq)]
230pub struct GroupKey {
231 pub expr: Expr,
232 pub output_name: String,
233}
234
235impl GroupKey {
236 pub fn output_name(&self) -> String {
240 self.output_name.clone()
241 }
242}
243
244#[derive(Debug, Clone, PartialEq)]
249pub struct JoinClause {
250 pub kind: JoinKind,
251 pub source: String,
252 pub alias: Option<String>,
253 pub on: Option<Expr>,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum JoinKind {
259 Inner,
260 LeftOuter,
261 RightOuter,
262 Cross,
263}
264
265#[derive(Debug, Clone, PartialEq)]
266pub struct ProjectionField {
267 pub alias: Option<String>,
268 pub expr: Expr,
269}
270
271#[derive(Debug, Clone, PartialEq)]
277pub struct NestedQuery {
278 pub source: String,
279 pub alias: String,
280 pub via_link: Option<ViaLink>,
286 pub filter: Expr,
287 pub order: Option<OrderClause>,
289 pub limit: Option<Expr>,
291 pub offset: Option<Expr>,
292 pub offset_before_limit: bool,
296 pub fields: Vec<ProjectionField>,
297}
298
299#[derive(Debug, Clone, PartialEq)]
300pub struct OrderClause {
301 pub keys: Vec<OrderKey>,
302}
303
304#[derive(Debug, Clone, PartialEq)]
305pub struct OrderKey {
306 pub expr: Expr,
307 pub descending: bool,
308}
309
310#[derive(Debug, Clone, PartialEq)]
311pub struct InsertExpr {
312 pub target: String,
313 pub rows: Vec<Vec<Assignment>>,
316 pub returning: bool,
319}
320
321#[derive(Debug, Clone, PartialEq)]
322pub struct UpdateExpr {
323 pub source: String,
324 pub alias: Option<String>,
327 pub filter: Option<Expr>,
328 pub assignments: Vec<Assignment>,
329 pub returning: bool,
332}
333
334#[derive(Debug, Clone, PartialEq)]
335pub struct DeleteExpr {
336 pub source: String,
337 pub alias: Option<String>,
340 pub filter: Option<Expr>,
341 pub returning: bool,
344}
345
346#[derive(Debug, Clone, PartialEq)]
347pub struct Assignment {
348 pub field: String,
349 pub value: Expr,
350}
351
352#[derive(Debug, Clone, PartialEq)]
353pub struct UpsertExpr {
355 pub target: String,
356 pub key_column: String,
357 pub assignments: Vec<Assignment>,
358 pub on_conflict: Vec<Assignment>,
361}
362
363#[derive(Debug, Clone, PartialEq)]
364pub struct CreateTypeExpr {
365 pub name: String,
366 pub fields: Vec<FieldDef>,
367 pub if_not_exists: bool,
370}
371
372#[derive(Debug, Clone, PartialEq)]
373pub struct FieldDef {
374 pub name: String,
375 pub type_name: String,
376 pub required: bool,
377 pub unique: bool,
380 pub default: Option<Literal>,
383 pub auto: bool,
386}
387
388#[derive(Debug, Clone, PartialEq)]
389pub struct AggregateExpr {
390 pub function: AggFunc,
391 pub argument: Option<Expr>,
392 pub mode: AggregateMode,
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
396pub enum AggregateMode {
397 Symmetric,
398 Raw,
399}
400
401#[derive(Debug, Clone, Copy, PartialEq)]
402pub enum AggFunc {
403 Count,
404 CountDistinct,
405 Avg,
406 Sum,
407 Min,
408 Max,
409}
410
411#[derive(Debug, Clone, Copy, PartialEq)]
413pub enum WindowFunc {
414 RowNumber,
415 Rank,
416 DenseRank,
417 Sum,
418 Avg,
419 Count,
420 Min,
421 Max,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq)]
426pub enum ScalarFn {
427 Upper,
428 Lower,
429 Length,
430 Trim,
431 Substring, Concat, Abs,
435 Round, Ceil,
437 Floor,
438 Sqrt,
439 Pow, Now, Extract, DateAdd, DateDiff, JsonType, JsonText, }
449
450#[derive(Debug, Clone, Copy, PartialEq)]
452pub enum CastType {
453 Int,
454 Float,
455 Str,
456 Bool,
457 DateTime,
458 Uuid,
459 Bytes,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Hash)]
470pub enum PathSeg {
471 Key(String),
473 Index(u32),
475}
476
477#[derive(Debug, Clone, PartialEq)]
479pub enum Expr {
480 Field(String),
481 QualifiedField {
486 qualifier: String,
487 field: String,
488 },
489 Literal(Literal),
490 Param(String),
491 BinaryOp(Box<Expr>, BinOp, Box<Expr>),
492 UnaryOp(UnaryOp, Box<Expr>),
493 FunctionCall(AggFunc, Box<Expr>, AggregateMode),
494 ScalarFunc(ScalarFn, Vec<Expr>),
496 Coalesce(Box<Expr>, Box<Expr>),
497 InList {
499 expr: Box<Expr>,
500 list: Vec<Expr>,
501 negated: bool,
502 },
503 InSubquery {
506 expr: Box<Expr>,
507 subquery: Box<QueryExpr>,
508 negated: bool,
509 },
510 ExistsSubquery {
514 subquery: Box<QueryExpr>,
515 negated: bool,
516 },
517 Case {
519 whens: Vec<(Box<Expr>, Box<Expr>)>,
520 else_expr: Option<Box<Expr>>,
521 },
522 Window {
524 function: WindowFunc,
525 args: Vec<Expr>,
526 mode: AggregateMode,
527 partition_by: Vec<Expr>,
528 order_by: Vec<OrderKey>,
529 },
530 Cast(Box<Expr>, CastType),
532 ValueLit(Value),
537 Null,
539 NestedQuery(Box<NestedQuery>),
543 LinkPath {
548 outer_alias: String,
550 links: Vec<String>,
553 column: String,
555 },
556 JsonPath {
561 base: Box<Expr>,
562 segments: Vec<PathSeg>,
563 },
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Hash)]
569pub struct JsonPathIdentityV1 {
570 pub root: JsonPathRootV1,
571 pub segments: Vec<PathSeg>,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Hash)]
575pub enum JsonPathRootV1 {
576 Unqualified(String),
577 Qualified { qualifier: String, field: String },
578}
579
580impl JsonPathIdentityV1 {
581 pub const VERSION: u8 = 1;
582
583 pub fn from_expr(expr: &Expr) -> Option<Self> {
584 let Expr::JsonPath { base, segments } = expr else {
585 return None;
586 };
587 let root = match base.as_ref() {
588 Expr::Field(field) => JsonPathRootV1::Unqualified(field.clone()),
589 Expr::QualifiedField { qualifier, field } => JsonPathRootV1::Qualified {
590 qualifier: qualifier.clone(),
591 field: field.clone(),
592 },
593 _ => return None,
594 };
595 Some(Self {
596 root,
597 segments: segments.clone(),
598 })
599 }
600
601 pub fn canonical_bytes(&self) -> Vec<u8> {
602 let mut out = vec![Self::VERSION];
603 match &self.root {
604 JsonPathRootV1::Unqualified(field) => {
605 out.push(1);
606 push_identity_str(&mut out, field);
607 }
608 JsonPathRootV1::Qualified { qualifier, field } => {
609 out.push(2);
610 push_identity_str(&mut out, qualifier);
611 push_identity_str(&mut out, field);
612 }
613 }
614 out.extend_from_slice(&(self.segments.len() as u32).to_le_bytes());
615 for segment in &self.segments {
616 match segment {
617 PathSeg::Key(key) => {
618 out.push(1);
619 push_identity_str(&mut out, key);
620 }
621 PathSeg::Index(index) => {
622 out.push(2);
623 out.extend_from_slice(&index.to_le_bytes());
624 }
625 }
626 }
627 out
628 }
629
630 pub fn canonical_text(&self) -> String {
631 let mut out = match &self.root {
632 JsonPathRootV1::Unqualified(field) => format!("v1:.{field}"),
633 JsonPathRootV1::Qualified { qualifier, field } => {
634 format!("v1:{qualifier}.{field}")
635 }
636 };
637 for segment in &self.segments {
638 match segment {
639 PathSeg::Key(key) => {
640 out.push_str("->\"");
641 push_identity_escaped(&mut out, key);
642 out.push('"');
643 }
644 PathSeg::Index(index) => {
645 out.push_str("->");
646 out.push_str(&index.to_string());
647 }
648 }
649 }
650 out
651 }
652
653 pub fn bind_table_local(
657 &self,
658 qualifier: Option<&str>,
659 ) -> Option<powdb_storage::stored_json_path::StoredJsonPathV1> {
660 use powdb_storage::stored_json_path::{StoredJsonPathSegmentV1, StoredJsonPathV1};
661 let column = match &self.root {
662 JsonPathRootV1::Unqualified(field) => field.clone(),
663 JsonPathRootV1::Qualified {
664 qualifier: actual,
665 field,
666 } if qualifier == Some(actual.as_str()) => field.clone(),
667 JsonPathRootV1::Qualified { .. } => return None,
668 };
669 Some(StoredJsonPathV1::new(
670 column,
671 self.segments
672 .iter()
673 .map(|segment| match segment {
674 PathSeg::Key(key) => StoredJsonPathSegmentV1::Key(key.clone()),
675 PathSeg::Index(index) => StoredJsonPathSegmentV1::Index(*index),
676 })
677 .collect(),
678 ))
679 }
680}
681
682pub fn projection_output_name(expr: &Expr) -> String {
692 match expr {
693 Expr::Field(name) => name.clone(),
696 Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
697 other => render_expr(other).unwrap_or_else(|| "?".into()),
698 }
699}
700
701fn render_expr(expr: &Expr) -> Option<String> {
702 Some(match expr {
703 Expr::Field(name) => format!(".{name}"),
704 Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
705 Expr::Literal(literal) => render_literal(literal),
706 Expr::Null => "null".into(),
707 Expr::Param(name) => format!("${name}"),
708 Expr::BinaryOp(left, op, right) => format!(
709 "{} {} {}",
710 render_expr(left)?,
711 render_binop(*op),
712 render_expr(right)?
713 ),
714 Expr::UnaryOp(op, inner) => match op {
715 UnaryOp::Not => format!("not {}", render_expr(inner)?),
716 UnaryOp::Exists => format!("exists {}", render_expr(inner)?),
717 UnaryOp::NotExists => format!("not exists {}", render_expr(inner)?),
718 UnaryOp::IsNull => format!("{} is null", render_expr(inner)?),
719 UnaryOp::IsNotNull => format!("{} is not null", render_expr(inner)?),
720 },
721 Expr::FunctionCall(func, argument, _) => match func {
722 AggFunc::CountDistinct => format!("count(distinct {})", render_expr(argument)?),
723 _ => format!("{}({})", render_agg(*func), render_expr(argument)?),
724 },
725 Expr::ScalarFunc(func, args) => {
726 let mut rendered = Vec::with_capacity(args.len());
727 for arg in args {
728 rendered.push(render_expr(arg)?);
729 }
730 format!("{}({})", render_scalar_fn(*func), rendered.join(", "))
731 }
732 Expr::Coalesce(left, right) => {
733 format!("coalesce({}, {})", render_expr(left)?, render_expr(right)?)
734 }
735 Expr::Cast(inner, target) => format!("{}({})", render_cast(*target), render_expr(inner)?),
736 Expr::JsonPath { base, segments } => {
737 let mut out = render_expr(base)?;
738 for segment in segments {
739 out.push_str("->");
740 match segment {
741 PathSeg::Key(key) => out.push_str(key),
742 PathSeg::Index(index) => out.push_str(&index.to_string()),
743 }
744 }
745 out
746 }
747 _ => return None,
748 })
749}
750
751fn render_literal(literal: &Literal) -> String {
752 match literal {
753 Literal::Int(v) => v.to_string(),
754 Literal::Float(v) => v.to_string(),
755 Literal::Bool(v) => v.to_string(),
756 Literal::String(s) => format!("\"{s}\""),
757 }
758}
759
760fn render_binop(op: BinOp) -> &'static str {
761 match op {
762 BinOp::Eq => "=",
763 BinOp::Neq => "!=",
764 BinOp::Lt => "<",
765 BinOp::Gt => ">",
766 BinOp::Lte => "<=",
767 BinOp::Gte => ">=",
768 BinOp::And => "and",
769 BinOp::Or => "or",
770 BinOp::Add => "+",
771 BinOp::Sub => "-",
772 BinOp::Mul => "*",
773 BinOp::Div => "/",
774 BinOp::Like => "like",
775 }
776}
777
778fn render_agg(func: AggFunc) -> &'static str {
779 match func {
780 AggFunc::Count | AggFunc::CountDistinct => "count",
781 AggFunc::Avg => "avg",
782 AggFunc::Sum => "sum",
783 AggFunc::Min => "min",
784 AggFunc::Max => "max",
785 }
786}
787
788fn render_scalar_fn(func: ScalarFn) -> &'static str {
789 match func {
790 ScalarFn::Upper => "upper",
791 ScalarFn::Lower => "lower",
792 ScalarFn::Length => "length",
793 ScalarFn::Trim => "trim",
794 ScalarFn::Substring => "substring",
795 ScalarFn::Concat => "concat",
796 ScalarFn::Abs => "abs",
797 ScalarFn::Round => "round",
798 ScalarFn::Ceil => "ceil",
799 ScalarFn::Floor => "floor",
800 ScalarFn::Sqrt => "sqrt",
801 ScalarFn::Pow => "pow",
802 ScalarFn::Now => "now",
803 ScalarFn::Extract => "extract",
804 ScalarFn::DateAdd => "date_add",
805 ScalarFn::DateDiff => "date_diff",
806 ScalarFn::JsonType => "json_type",
807 ScalarFn::JsonText => "json_text",
808 }
809}
810
811fn render_cast(target: CastType) -> &'static str {
812 match target {
813 CastType::Int => "int",
814 CastType::Float => "float",
815 CastType::Str => "str",
816 CastType::Bool => "bool",
817 CastType::DateTime => "datetime",
818 CastType::Uuid => "uuid",
819 CastType::Bytes => "bytes",
820 }
821}
822
823pub fn expression_output_name(expr: &Expr) -> String {
824 match expr {
825 Expr::Field(field) => field.clone(),
826 Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
827 Expr::JsonPath { .. } => JsonPathIdentityV1::from_expr(expr)
828 .map(|path| path.canonical_text())
829 .unwrap_or_else(|| "?".into()),
830 _ => "?".into(),
831 }
832}
833
834fn push_identity_str(out: &mut Vec<u8>, value: &str) {
835 out.extend_from_slice(&(value.len() as u32).to_le_bytes());
836 out.extend_from_slice(value.as_bytes());
837}
838
839fn push_identity_escaped(out: &mut String, value: &str) {
840 for ch in value.chars() {
841 match ch {
842 '"' => out.push_str("\\\""),
843 '\\' => out.push_str("\\\\"),
844 '\n' => out.push_str("\\n"),
845 '\r' => out.push_str("\\r"),
846 '\t' => out.push_str("\\t"),
847 c if c <= '\u{1f}' => {
848 use std::fmt::Write;
849 let _ = write!(out, "\\u{:04x}", c as u32);
850 }
851 c => out.push(c),
852 }
853 }
854}
855
856#[derive(Debug, Clone, PartialEq)]
857pub enum Literal {
858 Int(i64),
859 Float(f64),
860 String(String),
861 Bool(bool),
862}
863
864#[derive(Debug, Clone, PartialEq)]
872pub enum ParamValue {
873 Null,
874 Int(i64),
875 Float(f64),
876 Bool(bool),
877 Str(String),
878}
879
880#[derive(Debug, Clone, Copy, PartialEq)]
881pub enum BinOp {
882 Eq,
883 Neq,
884 Lt,
885 Gt,
886 Lte,
887 Gte,
888 And,
889 Or,
890 Add,
891 Sub,
892 Mul,
893 Div,
894 Like,
895}
896
897#[derive(Debug, Clone, Copy, PartialEq)]
898pub enum UnaryOp {
899 Not,
900 Exists,
901 NotExists,
902 IsNull,
903 IsNotNull,
904}
905
906#[cfg(test)]
907mod json_path_identity_tests {
908 use super::*;
909
910 #[test]
911 fn canonical_path_identity_goldens() {
912 let unquoted = JsonPathIdentityV1 {
913 root: JsonPathRootV1::Unqualified("data".into()),
914 segments: vec![PathSeg::Key("author".into())],
915 };
916 let quoted = JsonPathIdentityV1 {
917 root: JsonPathRootV1::Unqualified("data".into()),
918 segments: vec![PathSeg::Key("author".into())],
919 };
920 assert_eq!(unquoted, quoted);
921 assert_eq!(unquoted.canonical_text(), "v1:.data->\"author\"");
922 assert_eq!(
923 unquoted.canonical_bytes(),
924 vec![
925 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',
926 b't', b'h', b'o', b'r',
927 ]
928 );
929
930 let key_zero = JsonPathIdentityV1 {
931 root: JsonPathRootV1::Unqualified("data".into()),
932 segments: vec![PathSeg::Key("0".into())],
933 };
934 let index_zero = JsonPathIdentityV1 {
935 root: JsonPathRootV1::Unqualified("data".into()),
936 segments: vec![PathSeg::Index(0)],
937 };
938 assert_ne!(key_zero.canonical_bytes(), index_zero.canonical_bytes());
939
940 let unicode = JsonPathIdentityV1 {
941 root: JsonPathRootV1::Unqualified("data".into()),
942 segments: vec![PathSeg::Key("café\n\"x\\y".into())],
943 };
944 assert_eq!(unicode.canonical_text(), "v1:.data->\"café\\n\\\"x\\\\y\"");
945 }
946
947 #[test]
948 fn qualified_root_binding_is_explicit() {
949 let qualified = JsonPathIdentityV1 {
950 root: JsonPathRootV1::Qualified {
951 qualifier: "p".into(),
952 field: "data".into(),
953 },
954 segments: vec![PathSeg::Key("age".into())],
955 };
956 assert!(qualified.bind_table_local(Some("other")).is_none());
957 let stored = qualified
958 .bind_table_local(Some("p"))
959 .expect("matching root");
960 assert_eq!(stored.canonical_text(), "v1:.data->\"age\"");
961 }
962}