1use std::collections::{BTreeMap, BTreeSet};
54use std::fmt::Write as _;
55use std::sync::Arc;
56
57use crate::core::Value;
58use crate::plan::{OpId, Plan};
59use crate::split::Placed;
60use crate::ty::{Ty, TyDecl};
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum SqlTy {
73 Boolean,
74 Bigint,
75 Double,
76 Text,
77}
78
79impl SqlTy {
80 pub fn oid(self) -> u32 {
82 match self {
83 SqlTy::Boolean => 16,
84 SqlTy::Bigint => 20,
85 SqlTy::Text => 25,
86 SqlTy::Double => 701,
87 }
88 }
89
90 pub fn width(self) -> i16 {
92 match self {
93 SqlTy::Boolean => 1,
94 SqlTy::Bigint | SqlTy::Double => 8,
95 SqlTy::Text => -1,
96 }
97 }
98
99 pub fn name(self) -> &'static str {
100 match self {
101 SqlTy::Boolean => "boolean",
102 SqlTy::Bigint => "bigint",
103 SqlTy::Double => "double precision",
104 SqlTy::Text => "text",
105 }
106 }
107}
108
109#[derive(Clone, Debug)]
110pub struct Column {
111 pub name: Arc<str>,
112 pub ty: SqlTy,
113 pub nullable: bool,
116}
117
118#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum Source {
121 State(Vec<Arc<str>>),
126 View(OpId),
131 Catalogue,
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum Cardinality {
138 Many,
140 One,
142}
143
144#[derive(Clone, Debug)]
145pub struct Table {
146 pub name: Arc<str>,
147 pub columns: Vec<Column>,
148 pub source: Source,
149 pub cardinality: Cardinality,
150 pub element: Arc<str>,
152}
153
154impl Table {
155 pub fn column(&self, name: &str) -> Option<(usize, &Column)> {
156 self.columns
157 .iter()
158 .enumerate()
159 .find(|(_, c)| c.name.as_ref() == name)
160 }
161
162 pub fn row(&self, v: &Value) -> Vec<Cell> {
169 match (&self.cardinality, unwrap(v)) {
170 (_, Value::Data(d)) if d.variant.is_none() && !d.fields.is_empty() => self
172 .columns
173 .iter()
174 .map(|c| match d.fields.get(&c.name) {
175 Some(f) => cell(f, c),
176 None => None,
177 })
178 .collect(),
179 (_, other) => self
181 .columns
182 .iter()
183 .map(|c| cell(other, c))
184 .collect::<Vec<_>>(),
185 }
186 }
187}
188
189pub type Cell = Option<Datum>;
191
192#[derive(Clone, Debug, PartialEq)]
193pub enum Datum {
194 Boolean(bool),
195 Bigint(i64),
196 Double(f64),
197 Text(String),
198}
199
200impl Datum {
201 pub fn ty(&self) -> SqlTy {
202 match self {
203 Datum::Boolean(_) => SqlTy::Boolean,
204 Datum::Bigint(_) => SqlTy::Bigint,
205 Datum::Double(_) => SqlTy::Double,
206 Datum::Text(_) => SqlTy::Text,
207 }
208 }
209
210 pub fn text(&self) -> String {
213 match self {
214 Datum::Boolean(b) => if *b { "t" } else { "f" }.to_string(),
215 Datum::Bigint(i) => i.to_string(),
216 Datum::Double(f) => f.to_string(),
220 Datum::Text(s) => s.clone(),
221 }
222 }
223}
224
225fn cell(v: &Value, c: &Column) -> Cell {
227 let v = unwrap(v);
228 if let Value::Data(d) = v {
231 if d.variant.as_deref() == Some("None") {
232 return None;
233 }
234 if d.variant.as_deref() == Some("Some") {
235 return match d.fields.values().next() {
236 Some(inner) => cell(inner, c),
237 None => None,
238 };
239 }
240 }
241 match (c.ty, v) {
242 (SqlTy::Boolean, Value::Bool(b)) => Some(Datum::Boolean(*b)),
243 (SqlTy::Bigint, Value::Int(i)) => Some(Datum::Bigint(*i)),
244 (SqlTy::Double, _) => v.as_f64().map(Datum::Double),
245 (SqlTy::Text, Value::Str(s)) => Some(Datum::Text(s.to_string())),
246 (SqlTy::Text, other) => Some(Datum::Text(match other {
250 Value::Unit => return None,
251 _ => serde_json::to_string(&other.to_json()).unwrap_or_else(|_| other.display()),
252 })),
253 _ => None,
254 }
255}
256
257fn unwrap(v: &Value) -> &Value {
259 match v {
260 Value::Data(d) if d.variant.is_none() && d.fields.len() == 1 => {
261 match d.fields.values().next() {
262 Some(inner) => unwrap(inner),
263 None => v,
264 }
265 }
266 _ => v,
267 }
268}
269
270#[derive(Clone, Debug, Default)]
276pub struct Schema {
277 pub tables: Vec<Table>,
278}
279
280impl Schema {
281 pub const CATALOGUE: &'static str = "beck_columns";
284
285 pub fn table(&self, name: &str) -> Option<&Table> {
286 self.tables.iter().find(|t| t.name.as_ref() == name)
287 }
288
289 pub fn of(placed: &Placed, plan: &Plan) -> Schema {
291 let types = &placed.program.types;
292 let mut tables: Vec<Table> = Vec::new();
293 let mut taken: BTreeSet<Arc<str>> = BTreeSet::new();
294
295 for role in &placed.roles.states {
296 let base: Vec<Arc<str>> = role.field.iter().cloned().collect();
297 let ty = resolve(&role.ty, types);
298 match collection_elem(&ty, types) {
299 Some(elem) => push(
301 &mut tables,
302 &mut taken,
303 table(
304 role.name.clone(),
305 &elem,
306 types,
307 Source::State(base),
308 Cardinality::Many,
309 ),
310 ),
311 None => {
312 let fields = model_fields(&ty, types).unwrap_or_default();
313 let mut scalars: Vec<(Arc<str>, Ty)> = Vec::new();
314 for (name, fty) in fields {
315 let fty = resolve(&fty, types);
316 match collection_elem(&fty, types) {
317 Some(elem) => {
318 let mut path = base.clone();
319 path.push(name.clone());
320 push(
321 &mut tables,
322 &mut taken,
323 table(
324 name,
325 &elem,
326 types,
327 Source::State(path),
328 Cardinality::Many,
329 ),
330 );
331 }
332 None => scalars.push((name, fty)),
333 }
334 }
335 if !scalars.is_empty() {
340 push(
341 &mut tables,
342 &mut taken,
343 Table {
344 name: role.name.clone(),
345 columns: scalars
346 .iter()
347 .map(|(n, t)| column(n.clone(), t, types))
348 .collect(),
349 source: Source::State(base.clone()),
350 cardinality: Cardinality::One,
351 element: Arc::from(ty.to_string()),
352 },
353 );
354 }
355 }
356 }
357 }
358
359 let by_op: BTreeMap<&str, OpId> = plan
362 .signals
363 .iter()
364 .map(|(n, id)| (n.as_ref(), *id))
365 .collect();
366 let folds: BTreeSet<&str> = placed
367 .roles
368 .states
369 .iter()
370 .map(|s| s.name.as_ref())
371 .collect();
372 for (name, &sig) in &placed.graph.by_name {
373 let Some(&op) = by_op.get(name.as_ref()) else {
374 continue;
375 };
376 if plan.nodes[op].per_session {
377 continue;
378 }
379 if folds.contains(name.as_ref()) {
383 continue;
384 }
385 let ty = resolve(
386 &crate::signal::signal_elem(&placed.graph.node(sig).ty),
387 types,
388 );
389 let t = match collection_elem(&ty, types) {
390 Some(elem) => table(
391 name.clone(),
392 &elem,
393 types,
394 Source::View(op),
395 Cardinality::Many,
396 ),
397 None if model_fields(&ty, types).is_some() || scalar(&ty).is_some() => {
401 table(name.clone(), &ty, types, Source::View(op), Cardinality::One)
402 }
403 None => continue,
404 };
405 push(&mut tables, &mut taken, t);
406 }
407
408 tables.sort_by(|a, b| a.name.cmp(&b.name));
409 tables.push(Table {
410 name: Arc::from(Schema::CATALOGUE),
411 columns: [
412 "table_name",
413 "column_name",
414 "data_type",
415 "nullable",
416 "position",
417 ]
418 .iter()
419 .enumerate()
420 .map(|(i, n)| Column {
421 name: Arc::from(*n),
422 ty: if i == 4 {
423 SqlTy::Bigint
424 } else if i == 3 {
425 SqlTy::Boolean
426 } else {
427 SqlTy::Text
428 },
429 nullable: false,
430 })
431 .collect(),
432 source: Source::Catalogue,
433 cardinality: Cardinality::Many,
434 element: Arc::from("Column"),
435 });
436 Schema { tables }
437 }
438
439 pub fn catalogue_rows(&self) -> Vec<Vec<Cell>> {
441 let mut rows = Vec::new();
442 for t in &self.tables {
443 for (i, c) in t.columns.iter().enumerate() {
444 rows.push(vec![
445 Some(Datum::Text(t.name.to_string())),
446 Some(Datum::Text(c.name.to_string())),
447 Some(Datum::Text(c.ty.name().to_string())),
448 Some(Datum::Boolean(c.nullable)),
449 Some(Datum::Bigint(i as i64 + 1)),
450 ]);
451 }
452 }
453 rows
454 }
455
456 pub fn ddl(&self) -> String {
461 let mut out = String::new();
462 for t in &self.tables {
463 let what = match &t.source {
464 Source::State(path) if path.is_empty() => "the accumulator".to_string(),
465 Source::State(path) => format!("state.{}", join(path)),
466 Source::View(op) => format!("plan operator {op}, maintained and shared"),
467 Source::Catalogue => "this schema".to_string(),
468 };
469 let _ = writeln!(
470 out,
471 "-- {} of {}, from {what}",
472 match t.cardinality {
473 Cardinality::Many => "the elements",
474 Cardinality::One => "one row",
475 },
476 t.element
477 );
478 let _ = writeln!(out, "create table {} (", quote_ident(&t.name));
479 let n = t.columns.len();
480 for (i, c) in t.columns.iter().enumerate() {
481 let _ = writeln!(
482 out,
483 " {:<20} {}{}{}",
484 quote_ident(&c.name),
485 c.ty.name(),
486 if c.nullable { "" } else { " not null" },
487 if i + 1 == n { "" } else { "," }
488 );
489 }
490 let _ = writeln!(out, ");");
491 }
492 out
493 }
494}
495
496const RESERVED: &[&str] = &[
502 "abort", "and", "as", "asc", "begin", "by", "commit", "count", "desc", "discard", "distinct",
503 "end", "false", "from", "group", "is", "limit", "not", "null", "offset", "or", "order",
504 "rollback", "select", "set", "start", "table", "true", "where",
505];
506
507pub fn quote_ident(name: &str) -> String {
509 let plain = !name.is_empty()
510 && !name.starts_with(|c: char| c.is_ascii_digit())
511 && name
512 .chars()
513 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit());
514 if plain && !RESERVED.contains(&name) {
515 return name.to_string();
516 }
517 format!("\"{}\"", name.replace('"', "\"\""))
518}
519
520fn join(path: &[Arc<str>]) -> String {
521 path.iter()
522 .map(|p| p.to_string())
523 .collect::<Vec<_>>()
524 .join(".")
525}
526
527fn push(tables: &mut Vec<Table>, taken: &mut BTreeSet<Arc<str>>, t: Table) {
530 if taken.insert(t.name.clone()) {
531 tables.push(t);
532 }
533}
534
535fn table(
536 name: Arc<str>,
537 elem: &Ty,
538 types: &BTreeMap<Arc<str>, TyDecl>,
539 source: Source,
540 cardinality: Cardinality,
541) -> Table {
542 let elem = resolve(elem, types);
543 let columns = match model_fields(&elem, types) {
544 Some(fields) => fields
545 .into_iter()
546 .map(|(n, t)| column(n, &t, types))
547 .collect(),
548 None => vec![column(Arc::from("value"), &elem, types)],
550 };
551 Table {
552 name,
553 columns,
554 source,
555 cardinality,
556 element: Arc::from(elem.to_string()),
557 }
558}
559
560fn column(name: Arc<str>, ty: &Ty, types: &BTreeMap<Arc<str>, TyDecl>) -> Column {
561 let (ty, nullable) = sql_ty(ty, types);
562 Column { name, ty, nullable }
563}
564
565fn sql_ty(ty: &Ty, types: &BTreeMap<Arc<str>, TyDecl>) -> (SqlTy, bool) {
567 let ty = resolve(ty, types);
568 if let Ty::Con(n, args) = &ty {
569 if n.as_ref() == Ty::OPTION && args.len() == 1 {
570 return (sql_ty(&args[0], types).0, true);
571 }
572 }
573 (scalar(&ty).unwrap_or(SqlTy::Text), false)
574}
575
576fn scalar(ty: &Ty) -> Option<SqlTy> {
578 match ty {
579 Ty::Con(n, args) if args.is_empty() => match n.as_ref() {
580 Ty::INT => Some(SqlTy::Bigint),
581 Ty::FLOAT => Some(SqlTy::Double),
582 Ty::BOOL => Some(SqlTy::Boolean),
583 Ty::STR => Some(SqlTy::Text),
584 _ => None,
585 },
586 _ => None,
587 }
588}
589
590fn resolve(ty: &Ty, types: &BTreeMap<Arc<str>, TyDecl>) -> Ty {
593 let mut ty = ty.clone();
594 for _ in 0..16 {
597 let Ty::Con(name, args) = &ty else { return ty };
598 let next = match types.get(name) {
599 Some(TyDecl::Newtype { params, inner, .. }) => substitute(inner, params, args),
600 Some(TyDecl::Alias { params, ty: t, .. }) => substitute(t, params, args),
601 _ => return ty,
602 };
603 ty = next;
604 }
605 ty
606}
607
608fn substitute(ty: &Ty, params: &[Arc<str>], args: &[Ty]) -> Ty {
609 if params.is_empty() {
610 return ty.clone();
611 }
612 match ty {
613 Ty::Con(n, inner) if inner.is_empty() => match params.iter().position(|p| p == n) {
614 Some(i) if i < args.len() => args[i].clone(),
615 _ => ty.clone(),
616 },
617 Ty::Con(n, inner) => Ty::Con(
618 n.clone(),
619 inner.iter().map(|t| substitute(t, params, args)).collect(),
620 ),
621 _ => ty.clone(),
622 }
623}
624
625fn collection_elem(ty: &Ty, types: &BTreeMap<Arc<str>, TyDecl>) -> Option<Ty> {
627 match resolve(ty, types) {
628 Ty::Con(n, args) if n.as_ref() == Ty::LIST && args.len() == 1 => Some(args[0].clone()),
629 Ty::Con(n, args) if n.as_ref() == Ty::MAP && args.len() == 2 => Some(args[1].clone()),
630 _ => None,
631 }
632}
633
634fn model_fields(ty: &Ty, types: &BTreeMap<Arc<str>, TyDecl>) -> Option<Vec<(Arc<str>, Ty)>> {
641 let Ty::Con(name, args) = resolve(ty, types) else {
642 return None;
643 };
644 match types.get(&name) {
645 Some(TyDecl::Model { params, fields, .. }) if !fields.is_empty() => Some(
646 fields
647 .iter()
648 .map(|(n, t)| (n.clone(), substitute(t, params, &args)))
649 .collect(),
650 ),
651 _ => None,
652 }
653}
654
655pub fn elements(v: &Value) -> Vec<Value> {
657 match v {
658 Value::List(xs) => xs.as_ref().clone(),
659 Value::Map(m) => m.iter().map(|(_, v)| v.clone()).collect(),
660 other => vec![other.clone()],
661 }
662}
663
664pub fn at_path(v: &Value, path: &[Arc<str>]) -> Option<Value> {
666 let mut cur = v.clone();
667 for step in path {
668 let Value::Data(d) = &cur else { return None };
669 cur = d.fields.get(step)?.clone();
670 }
671 Some(cur)
672}
673
674#[derive(Clone, Debug)]
684pub struct Select {
685 pub items: Vec<Item>,
686 pub from: Option<String>,
687 pub filter: Vec<Vec<Cond>>,
688 pub order: Option<(String, bool)>,
689 pub limit: Option<usize>,
690 pub offset: usize,
691}
692
693#[derive(Clone, Debug)]
694pub enum Item {
695 All,
696 Column(String, Option<String>),
697 Count(Option<String>),
698 Literal(Datum, Option<String>),
699}
700
701#[derive(Clone, Debug)]
702pub struct Cond {
703 pub column: String,
704 pub op: CmpOp,
705 pub value: Option<Datum>,
706}
707
708#[derive(Clone, Copy, Debug, PartialEq, Eq)]
709pub enum CmpOp {
710 Eq,
711 Ne,
712 Lt,
713 Le,
714 Gt,
715 Ge,
716 Is,
717 IsNot,
718}
719
720#[derive(Clone, Debug)]
723pub enum Stmt {
724 Select(Select),
725 Ignored(&'static str),
729}
730
731pub struct Answer {
733 pub columns: Vec<Column>,
734 pub rows: Vec<Vec<Cell>>,
735 pub tag: String,
737}
738
739#[derive(Clone, Debug, PartialEq, Eq)]
741pub struct SqlError {
742 pub message: String,
743 pub code: &'static str,
745}
746
747impl SqlError {
748 fn syntax(m: impl Into<String>) -> SqlError {
749 SqlError {
750 message: m.into(),
751 code: "42601",
752 }
753 }
754 fn no_table(m: impl Into<String>) -> SqlError {
755 SqlError {
756 message: m.into(),
757 code: "42P01",
758 }
759 }
760 fn no_column(m: impl Into<String>) -> SqlError {
761 SqlError {
762 message: m.into(),
763 code: "42703",
764 }
765 }
766 fn unsupported(m: impl Into<String>) -> SqlError {
767 SqlError {
768 message: m.into(),
769 code: "0A000",
770 }
771 }
772}
773
774impl std::fmt::Display for SqlError {
775 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
776 f.write_str(&self.message)
777 }
778}
779
780impl std::error::Error for SqlError {}
781
782pub trait Rows {
784 fn scan(&self, table: &Table) -> Result<Vec<Value>, SqlError>;
786}
787
788impl Schema {
789 pub fn run(&self, sql: &str, rows: &dyn Rows) -> Result<Answer, SqlError> {
791 match parse(sql)? {
792 Stmt::Ignored(tag) => Ok(Answer {
793 columns: Vec::new(),
794 rows: Vec::new(),
795 tag: tag.to_string(),
796 }),
797 Stmt::Select(s) => self.select(&s, rows),
798 }
799 }
800
801 pub fn describe(&self, sql: &str) -> Result<Vec<Column>, SqlError> {
804 match parse(sql)? {
805 Stmt::Ignored(_) => Ok(Vec::new()),
806 Stmt::Select(s) => {
807 let table = self.resolve_from(&s)?;
808 Ok(self.project_columns(&s, table)?.0)
809 }
810 }
811 }
812
813 fn resolve_from(&self, s: &Select) -> Result<Option<&Table>, SqlError> {
814 match &s.from {
815 None => Ok(None),
816 Some(name) => match self.table(name) {
817 Some(t) => Ok(Some(t)),
818 None => Err(SqlError::no_table(format!(
819 "there is no read model called \"{name}\". \
820 `select table_name from {} group by`— no: this SQL has no group by; \
821 `select * from {}` lists what there is",
822 Schema::CATALOGUE,
823 Schema::CATALOGUE
824 ))),
825 },
826 }
827 }
828
829 fn project_columns(
831 &self,
832 s: &Select,
833 table: Option<&Table>,
834 ) -> Result<(Vec<Column>, Vec<Proj>), SqlError> {
835 let mut columns = Vec::new();
836 let mut proj = Vec::new();
837 for item in &s.items {
838 match item {
839 Item::All => {
840 let Some(t) = table else {
841 return Err(SqlError::syntax("`select *` needs a `from`"));
842 };
843 for (i, c) in t.columns.iter().enumerate() {
844 columns.push(c.clone());
845 proj.push(Proj::Column(i));
846 }
847 }
848 Item::Column(name, alias) => {
849 let Some(t) = table else {
850 return Err(SqlError::no_column(format!(
851 "there is no column \"{name}\" here, because there is no `from`"
852 )));
853 };
854 let (i, c) = t.column(name).ok_or_else(|| {
855 SqlError::no_column(format!(
856 "\"{}\" has no column \"{name}\"; it has {}",
857 t.name,
858 names(&t.columns)
859 ))
860 })?;
861 let mut c = c.clone();
862 if let Some(a) = alias {
863 c.name = Arc::from(a.as_str());
864 }
865 columns.push(c);
866 proj.push(Proj::Column(i));
867 }
868 Item::Count(alias) => {
869 columns.push(Column {
870 name: Arc::from(alias.as_deref().unwrap_or("count")),
871 ty: SqlTy::Bigint,
872 nullable: false,
873 });
874 proj.push(Proj::Count);
875 }
876 Item::Literal(d, alias) => {
877 columns.push(Column {
878 name: Arc::from(alias.as_deref().unwrap_or("?column?")),
879 ty: d.ty(),
880 nullable: false,
881 });
882 proj.push(Proj::Literal(d.clone()));
883 }
884 }
885 }
886 Ok((columns, proj))
887 }
888
889 fn select(&self, s: &Select, rows_of: &dyn Rows) -> Result<Answer, SqlError> {
890 let table = self.resolve_from(s)?;
891 let (columns, proj) = self.project_columns(s, table)?;
892
893 let Some(t) = table else {
896 let row: Vec<Cell> = proj
897 .iter()
898 .map(|p| match p {
899 Proj::Literal(d) => Some(d.clone()),
900 Proj::Count => Some(Datum::Bigint(1)),
901 Proj::Column(_) => None,
902 })
903 .collect();
904 return Ok(Answer {
905 tag: "SELECT 1".to_string(),
906 columns,
907 rows: vec![row],
908 });
909 };
910
911 let mut rows: Vec<Vec<Cell>> = match &t.source {
912 Source::Catalogue => self.catalogue_rows(),
913 _ => {
914 let values = rows_of.scan(t)?;
915 match t.cardinality {
916 Cardinality::Many => values.iter().map(|v| t.row(v)).collect(),
917 Cardinality::One => values.iter().take(1).map(|v| t.row(v)).collect(),
920 }
921 }
922 };
923
924 for disjunction in &s.filter {
927 let mut tested = Vec::with_capacity(rows.len());
928 for row in rows {
929 if any_matches(t, disjunction, &row)? {
930 tested.push(row);
931 }
932 }
933 rows = tested;
934 }
935 if let Some((name, asc)) = &s.order {
936 let (i, _) = t.column(name).ok_or_else(|| {
937 SqlError::no_column(format!(
938 "cannot order \"{}\" by \"{name}\"; it has {}",
939 t.name,
940 names(&t.columns)
941 ))
942 })?;
943 rows.sort_by(|a, b| {
946 let o = compare(&a[i], &b[i]);
947 if *asc {
948 o
949 } else {
950 o.reverse()
951 }
952 });
953 }
954 if s.offset > 0 {
955 rows = rows.split_off(s.offset.min(rows.len()));
956 }
957 if let Some(n) = s.limit {
958 rows.truncate(n);
959 }
960
961 let out: Vec<Vec<Cell>> = if proj.iter().any(|p| matches!(p, Proj::Count)) {
964 vec![proj
965 .iter()
966 .map(|p| match p {
967 Proj::Count => Some(Datum::Bigint(rows.len() as i64)),
968 Proj::Literal(d) => Some(d.clone()),
969 Proj::Column(_) => None,
970 })
971 .collect()]
972 } else {
973 rows.iter()
974 .map(|r| {
975 proj.iter()
976 .map(|p| match p {
977 Proj::Column(i) => r[*i].clone(),
978 Proj::Literal(d) => Some(d.clone()),
979 Proj::Count => None,
980 })
981 .collect()
982 })
983 .collect()
984 };
985 Ok(Answer {
986 tag: format!("SELECT {}", out.len()),
987 columns,
988 rows: out,
989 })
990 }
991}
992
993enum Proj {
994 Column(usize),
995 Count,
996 Literal(Datum),
997}
998
999fn names(columns: &[Column]) -> String {
1000 columns
1001 .iter()
1002 .map(|c| format!("\"{}\"", c.name))
1003 .collect::<Vec<_>>()
1004 .join(", ")
1005}
1006
1007fn any_matches(t: &Table, conds: &[Cond], row: &[Cell]) -> Result<bool, SqlError> {
1008 for c in conds {
1009 let (i, _) = t.column(&c.column).ok_or_else(|| {
1010 SqlError::no_column(format!(
1011 "\"{}\" has no column \"{}\"; it has {}",
1012 t.name,
1013 c.column,
1014 names(&t.columns)
1015 ))
1016 })?;
1017 if matches_one(&row[i], c) {
1018 return Ok(true);
1019 }
1020 }
1021 Ok(false)
1022}
1023
1024fn matches_one(cell: &Cell, c: &Cond) -> bool {
1025 match c.op {
1026 CmpOp::Is => cell.is_none() == c.value.is_none() && (c.value.is_none() || equal(cell, c)),
1027 CmpOp::IsNot => {
1028 !(cell.is_none() == c.value.is_none() && (c.value.is_none() || equal(cell, c)))
1029 }
1030 _ => match (cell, &c.value) {
1033 (Some(a), Some(b)) => {
1034 let o = compare(&Some(a.clone()), &Some(b.clone()));
1035 match c.op {
1036 CmpOp::Eq => o.is_eq(),
1037 CmpOp::Ne => o.is_ne(),
1038 CmpOp::Lt => o.is_lt(),
1039 CmpOp::Le => o.is_le(),
1040 CmpOp::Gt => o.is_gt(),
1041 CmpOp::Ge => o.is_ge(),
1042 CmpOp::Is | CmpOp::IsNot => false,
1043 }
1044 }
1045 _ => false,
1046 },
1047 }
1048}
1049
1050fn equal(cell: &Cell, c: &Cond) -> bool {
1051 compare(cell, &c.value).is_eq()
1052}
1053
1054fn compare(a: &Cell, b: &Cell) -> std::cmp::Ordering {
1056 use std::cmp::Ordering;
1057 match (a, b) {
1058 (None, None) => Ordering::Equal,
1059 (None, Some(_)) => Ordering::Greater,
1060 (Some(_), None) => Ordering::Less,
1061 (Some(x), Some(y)) => match (x, y) {
1062 (Datum::Bigint(p), Datum::Bigint(q)) => p.cmp(q),
1063 (Datum::Double(p), Datum::Double(q)) => p.partial_cmp(q).unwrap_or(Ordering::Equal),
1064 (Datum::Bigint(p), Datum::Double(q)) => {
1065 (*p as f64).partial_cmp(q).unwrap_or(Ordering::Equal)
1066 }
1067 (Datum::Double(p), Datum::Bigint(q)) => {
1068 p.partial_cmp(&(*q as f64)).unwrap_or(Ordering::Equal)
1069 }
1070 (Datum::Boolean(p), Datum::Boolean(q)) => p.cmp(q),
1071 (Datum::Text(p), Datum::Text(q)) => p.cmp(q),
1072 _ => x.text().cmp(&y.text()),
1075 },
1076 }
1077}
1078
1079#[derive(Clone, Debug, PartialEq)]
1084enum Tok {
1085 Word(String),
1086 Quoted(String),
1087 Str(String),
1088 Num(String),
1089 Sym(String),
1090}
1091
1092fn lex(sql: &str) -> Result<Vec<Tok>, SqlError> {
1093 let cs: Vec<char> = sql.chars().collect();
1094 let mut i = 0;
1095 let mut out = Vec::new();
1096 while i < cs.len() {
1097 let c = cs[i];
1098 if c.is_whitespace() {
1099 i += 1;
1100 } else if c == '-' && cs.get(i + 1) == Some(&'-') {
1101 while i < cs.len() && cs[i] != '\n' {
1102 i += 1;
1103 }
1104 } else if c == '_' || c.is_alphabetic() {
1105 let start = i;
1106 while i < cs.len() && (cs[i] == '_' || cs[i] == '$' || cs[i].is_alphanumeric()) {
1107 i += 1;
1108 }
1109 out.push(Tok::Word(cs[start..i].iter().collect()));
1110 } else if c.is_ascii_digit()
1111 || (c == '.' && cs.get(i + 1).is_some_and(char::is_ascii_digit))
1112 {
1113 let start = i;
1114 while i < cs.len() && (cs[i].is_ascii_digit() || cs[i] == '.') {
1115 i += 1;
1116 }
1117 out.push(Tok::Num(cs[start..i].iter().collect()));
1118 } else if c == '\'' {
1119 i += 1;
1120 let mut s = String::new();
1121 loop {
1122 match cs.get(i) {
1123 None => return Err(SqlError::syntax("a string literal is not closed")),
1124 Some('\'') if cs.get(i + 1) == Some(&'\'') => {
1126 s.push('\'');
1127 i += 2;
1128 }
1129 Some('\'') => {
1130 i += 1;
1131 break;
1132 }
1133 Some(ch) => {
1134 s.push(*ch);
1135 i += 1;
1136 }
1137 }
1138 }
1139 out.push(Tok::Str(s));
1140 } else if c == '"' {
1141 i += 1;
1142 let mut s = String::new();
1143 loop {
1144 match cs.get(i) {
1145 None => return Err(SqlError::syntax("a quoted name is not closed")),
1146 Some('"') if cs.get(i + 1) == Some(&'"') => {
1147 s.push('"');
1148 i += 2;
1149 }
1150 Some('"') => {
1151 i += 1;
1152 break;
1153 }
1154 Some(ch) => {
1155 s.push(*ch);
1156 i += 1;
1157 }
1158 }
1159 }
1160 out.push(Tok::Quoted(s));
1161 } else {
1162 let two: String = cs[i..(i + 2).min(cs.len())].iter().collect();
1164 if matches!(two.as_str(), "<=" | ">=" | "<>" | "!=") {
1165 out.push(Tok::Sym(two));
1166 i += 2;
1167 } else {
1168 out.push(Tok::Sym(c.to_string()));
1169 i += 1;
1170 }
1171 }
1172 }
1173 Ok(out)
1174}
1175
1176struct P {
1177 toks: Vec<Tok>,
1178 i: usize,
1179}
1180
1181impl P {
1182 fn peek(&self) -> Option<&Tok> {
1183 self.toks.get(self.i)
1184 }
1185
1186 fn keyword(&self) -> Option<String> {
1188 match self.peek() {
1189 Some(Tok::Word(w)) => Some(w.to_lowercase()),
1190 _ => None,
1191 }
1192 }
1193
1194 fn eat_keyword(&mut self, k: &str) -> bool {
1195 if self.keyword().as_deref() == Some(k) {
1196 self.i += 1;
1197 return true;
1198 }
1199 false
1200 }
1201
1202 fn eat_sym(&mut self, s: &str) -> bool {
1203 if self.peek() == Some(&Tok::Sym(s.to_string())) {
1204 self.i += 1;
1205 return true;
1206 }
1207 false
1208 }
1209
1210 fn name(&mut self) -> Option<String> {
1213 match self.peek().cloned() {
1214 Some(Tok::Word(w)) => {
1215 self.i += 1;
1216 Some(w.to_lowercase())
1217 }
1218 Some(Tok::Quoted(w)) => {
1219 self.i += 1;
1220 Some(w)
1221 }
1222 _ => None,
1223 }
1224 }
1225
1226 fn literal(&mut self) -> Option<Option<Datum>> {
1227 match self.peek().cloned() {
1228 Some(Tok::Str(s)) => {
1229 self.i += 1;
1230 Some(Some(Datum::Text(s)))
1231 }
1232 Some(Tok::Num(n)) => {
1233 self.i += 1;
1234 Some(Some(match n.parse::<i64>() {
1235 Ok(i) => Datum::Bigint(i),
1236 Err(_) => Datum::Double(n.parse::<f64>().unwrap_or(0.0)),
1237 }))
1238 }
1239 Some(Tok::Sym(s)) if s == "-" => {
1240 self.i += 1;
1241 match self.literal() {
1242 Some(Some(Datum::Bigint(i))) => Some(Some(Datum::Bigint(-i))),
1243 Some(Some(Datum::Double(f))) => Some(Some(Datum::Double(-f))),
1244 _ => None,
1245 }
1246 }
1247 Some(Tok::Word(w)) => match w.to_lowercase().as_str() {
1248 "true" => {
1249 self.i += 1;
1250 Some(Some(Datum::Boolean(true)))
1251 }
1252 "false" => {
1253 self.i += 1;
1254 Some(Some(Datum::Boolean(false)))
1255 }
1256 "null" => {
1257 self.i += 1;
1258 Some(None)
1259 }
1260 _ => None,
1261 },
1262 _ => None,
1263 }
1264 }
1265}
1266
1267pub fn parse(sql: &str) -> Result<Stmt, SqlError> {
1269 let toks = lex(sql)?;
1270 let mut p = P { toks, i: 0 };
1271 let head = p.keyword().unwrap_or_default();
1272 match head.as_str() {
1273 "select" => {
1274 p.i += 1;
1275 let s = select(&mut p)?;
1276 p.eat_sym(";");
1279 if p.peek().is_some() {
1280 return Err(SqlError::unsupported(
1281 "one statement per query: this SQL has no multi-statement form",
1282 ));
1283 }
1284 Ok(Stmt::Select(s))
1285 }
1286 "set" => Ok(Stmt::Ignored("SET")),
1287 "begin" | "start" => Ok(Stmt::Ignored("BEGIN")),
1288 "commit" | "end" => Ok(Stmt::Ignored("COMMIT")),
1289 "rollback" | "abort" => Ok(Stmt::Ignored("ROLLBACK")),
1290 "discard" => Ok(Stmt::Ignored("DISCARD ALL")),
1291 "" => Err(SqlError::syntax("an empty query")),
1292 other => Err(SqlError::unsupported(format!(
1293 "a read model is read-only and this SQL is a subset: `{other}` is not one of \
1294 select, set, begin, commit, rollback"
1295 ))),
1296 }
1297}
1298
1299fn select(p: &mut P) -> Result<Select, SqlError> {
1300 if p.eat_keyword("distinct") {
1302 return Err(SqlError::unsupported(
1303 "`distinct` is not in this SQL subset",
1304 ));
1305 }
1306 let mut items = Vec::new();
1307 loop {
1308 items.push(item(p)?);
1309 if !p.eat_sym(",") {
1310 break;
1311 }
1312 }
1313 if items.iter().filter(|i| matches!(i, Item::Count(_))).count() > 0
1314 && items
1315 .iter()
1316 .any(|i| matches!(i, Item::All | Item::Column(_, _)))
1317 {
1318 return Err(SqlError::unsupported(
1319 "`count(*)` beside a column would need a `group by`, and this SQL has none",
1320 ));
1321 }
1322
1323 let mut from = None;
1324 if p.eat_keyword("from") {
1325 from = Some(
1326 p.name()
1327 .ok_or_else(|| SqlError::syntax("`from` wants a table name"))?,
1328 );
1329 }
1330
1331 let mut filter = Vec::new();
1332 if p.eat_keyword("where") {
1333 filter = where_clause(p)?;
1334 }
1335
1336 let mut order = None;
1337 if p.eat_keyword("order") {
1338 if !p.eat_keyword("by") {
1339 return Err(SqlError::syntax("`order` wants `by`"));
1340 }
1341 let col = p
1342 .name()
1343 .ok_or_else(|| SqlError::syntax("`order by` wants a column"))?;
1344 let asc = if p.eat_keyword("desc") {
1345 false
1346 } else {
1347 p.eat_keyword("asc");
1348 true
1349 };
1350 order = Some((col, asc));
1351 }
1352
1353 let mut limit = None;
1354 let mut offset = 0;
1355 loop {
1356 if p.eat_keyword("limit") {
1357 match p.literal() {
1358 Some(Some(Datum::Bigint(n))) if n >= 0 => limit = Some(n as usize),
1359 _ => return Err(SqlError::syntax("`limit` wants a whole number")),
1360 }
1361 } else if p.eat_keyword("offset") {
1362 match p.literal() {
1363 Some(Some(Datum::Bigint(n))) if n >= 0 => offset = n as usize,
1364 _ => return Err(SqlError::syntax("`offset` wants a whole number")),
1365 }
1366 } else {
1367 break;
1368 }
1369 }
1370
1371 Ok(Select {
1372 items,
1373 from,
1374 filter,
1375 order,
1376 limit,
1377 offset,
1378 })
1379}
1380
1381fn item(p: &mut P) -> Result<Item, SqlError> {
1382 if p.eat_sym("*") {
1383 return Ok(Item::All);
1384 }
1385 if let Some(lit) = p.literal() {
1386 let d = lit.ok_or_else(|| SqlError::unsupported("`select null` has no column type"))?;
1387 return Ok(Item::Literal(d, alias(p)));
1388 }
1389 let name = p
1390 .name()
1391 .ok_or_else(|| SqlError::syntax("a select list wants a column, `*`, or a literal"))?;
1392 if p.eat_sym("(") {
1393 let f = match name.as_str() {
1396 "count" => {
1397 if !p.eat_sym("*") {
1398 return Err(SqlError::unsupported(
1399 "`count` counts rows here: `count(*)` is the only form",
1400 ));
1401 }
1402 Item::Count(None)
1403 }
1404 "version" => Item::Literal(Datum::Text(version()), None),
1405 "current_database" | "current_schema" | "current_catalog" => {
1406 Item::Literal(Datum::Text("beck".into()), None)
1407 }
1408 other => {
1409 return Err(SqlError::unsupported(format!(
1410 "`{other}(…)` is not a function this read model has"
1411 )))
1412 }
1413 };
1414 if !p.eat_sym(")") {
1415 return Err(SqlError::syntax("a call is not closed"));
1416 }
1417 let a = alias(p);
1418 return Ok(match f {
1419 Item::Count(_) => Item::Count(a),
1420 Item::Literal(d, _) => Item::Literal(d, a.or(Some(name))),
1421 other => other,
1422 });
1423 }
1424 Ok(Item::Column(name.clone(), alias(p)))
1425}
1426
1427fn alias(p: &mut P) -> Option<String> {
1428 if p.eat_keyword("as") {
1429 return p.name();
1430 }
1431 match p.keyword().as_deref() {
1433 Some("from") | Some("where") | Some("order") | Some("limit") | Some("offset")
1434 | Some("as") | None => None,
1435 Some(_) => p.name(),
1436 }
1437}
1438
1439fn where_clause(p: &mut P) -> Result<Vec<Vec<Cond>>, SqlError> {
1445 let mut and_groups: Vec<Vec<Cond>> = vec![vec![cond(p)?]];
1446 loop {
1447 if p.eat_keyword("and") {
1448 and_groups.push(vec![cond(p)?]);
1449 } else if p.eat_keyword("or") {
1450 let c = cond(p)?;
1451 and_groups
1452 .last_mut()
1453 .expect("there is always one group")
1454 .push(c);
1455 } else {
1456 return Ok(and_groups);
1457 }
1458 }
1459}
1460
1461fn cond(p: &mut P) -> Result<Cond, SqlError> {
1462 if p.eat_sym("(") {
1463 return Err(SqlError::unsupported(
1464 "a parenthesised condition is not in this SQL subset",
1465 ));
1466 }
1467 let column = p
1468 .name()
1469 .ok_or_else(|| SqlError::syntax("`where` wants a column"))?;
1470 let op = if p.eat_keyword("is") {
1471 if p.eat_keyword("not") {
1472 CmpOp::IsNot
1473 } else {
1474 CmpOp::Is
1475 }
1476 } else if p.eat_sym("=") {
1477 CmpOp::Eq
1478 } else if p.eat_sym("<>") || p.eat_sym("!=") {
1479 CmpOp::Ne
1480 } else if p.eat_sym("<=") {
1481 CmpOp::Le
1482 } else if p.eat_sym(">=") {
1483 CmpOp::Ge
1484 } else if p.eat_sym("<") {
1485 CmpOp::Lt
1486 } else if p.eat_sym(">") {
1487 CmpOp::Gt
1488 } else {
1489 return Err(SqlError::unsupported(format!(
1490 "the comparisons here are =, <>, <, <=, >, >= and `is null`; \
1491 \"{column}\" is followed by none of them"
1492 )));
1493 };
1494 let value = p
1495 .literal()
1496 .ok_or_else(|| SqlError::syntax(format!("\"{column}\" is compared against nothing")))?;
1497 Ok(Cond { column, op, value })
1498}
1499
1500pub fn version() -> String {
1506 format!(
1507 "PostgreSQL 15.0 (beck {}) — a read model, not a database",
1508 env!("CARGO_PKG_VERSION")
1509 )
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514 use super::*;
1515
1516 fn ok(sql: &str) -> Select {
1517 match parse(sql).expect("parses") {
1518 Stmt::Select(s) => s,
1519 other => panic!("not a select: {other:?}"),
1520 }
1521 }
1522
1523 #[test]
1524 fn a_select_is_case_folded_and_a_quoted_name_is_not() {
1525 let s = ok("SELECT Text FROM Todos");
1526 assert_eq!(s.from.as_deref(), Some("todos"));
1527 assert!(matches!(&s.items[0], Item::Column(c, _) if c == "text"));
1528 let s = ok(r#"select "Text" from "Todos""#);
1529 assert_eq!(s.from.as_deref(), Some("Todos"));
1530 assert!(matches!(&s.items[0], Item::Column(c, _) if c == "Text"));
1531 }
1532
1533 #[test]
1534 fn and_binds_tighter_than_or() {
1535 let s = ok("select * from t where a = 1 or b = 2 and c = 3");
1537 assert_eq!(s.filter.len(), 2);
1538 assert_eq!(s.filter[0].len(), 2);
1539 assert_eq!(s.filter[1].len(), 1);
1540 }
1541
1542 #[test]
1543 fn a_negative_literal_is_one_number() {
1544 let s = ok("select * from t where n < -3");
1545 assert_eq!(s.filter[0][0].value, Some(Datum::Bigint(-3)));
1546 }
1547
1548 #[test]
1549 fn an_escaped_quote_is_one_character() {
1550 let s = ok("select * from t where name = 'it''s'");
1551 assert_eq!(s.filter[0][0].value, Some(Datum::Text("it's".to_string())));
1552 }
1553
1554 #[test]
1555 fn a_write_is_refused_by_name() {
1556 let e = parse("insert into todos values (1)").expect_err("refused");
1557 assert_eq!(e.code, "0A000");
1558 assert!(e.message.contains("read-only"), "{}", e.message);
1559 }
1560
1561 #[test]
1562 fn count_beside_a_column_is_refused_rather_than_answered() {
1563 let e = parse("select id, count(*) from todos").expect_err("refused");
1564 assert!(e.message.contains("group by"), "{}", e.message);
1565 }
1566
1567 #[test]
1568 fn a_second_statement_is_refused() {
1569 assert!(parse("select 1; select 2").is_err());
1570 }
1571
1572 #[test]
1573 fn nulls_sort_last_and_compare_as_unknown() {
1574 let c = Cond {
1575 column: "x".into(),
1576 op: CmpOp::Eq,
1577 value: Some(Datum::Bigint(1)),
1578 };
1579 assert!(!matches_one(&None, &c));
1580 let is_null = Cond {
1581 column: "x".into(),
1582 op: CmpOp::Is,
1583 value: None,
1584 };
1585 assert!(matches_one(&None, &is_null));
1586 assert!(!matches_one(&Some(Datum::Bigint(1)), &is_null));
1587 assert!(compare(&None, &Some(Datum::Bigint(1))).is_gt());
1588 }
1589}