1#![no_std]
6
7extern crate alloc;
8
9pub mod aggregate;
10pub mod describe;
11pub mod eval;
12pub mod json;
13pub mod memoize;
14pub mod plan_cache;
15pub mod publications;
16pub mod query_stats;
17pub mod reorder;
18pub mod selectivity;
19pub mod statistics;
20pub mod subscriptions;
21pub mod users;
22
23pub use crate::users::{Role, ScramSecrets, UserError, UserStore};
24
25use alloc::borrow::Cow;
26use alloc::boxed::Box;
27use alloc::collections::BTreeMap;
28use alloc::string::{String, ToString};
29use alloc::vec::Vec;
30use core::fmt;
31
32use spg_sql::ast::{
33 BinOp, ColumnDef, ColumnName, ColumnTypeName, CreateIndexStatement,
34 CreatePublicationStatement, CreateSubscriptionStatement, CreateTableStatement,
35 CreateUserStatement, Expr, FrameBound, FrameKind, FromClause, IndexMethod, InsertStatement,
36 JoinKind, Literal, OrderBy, SelectItem, SelectStatement, Statement, UnOp, UnionKind,
37 VecEncoding as SqlVecEncoding, WindowFrame,
38};
39use spg_sql::parser::{self, ParseError};
40use spg_storage::{
41 Catalog, ColumnSchema, CompactReport, DataType, IndexKey, IndexKind, Row, StorageError, Table,
42 TableSchema, Value, VecEncoding,
43};
44
45use crate::eval::{EvalContext, EvalError};
46
47#[derive(Debug, Clone, PartialEq)]
49#[non_exhaustive]
50pub enum QueryResult {
51 CommandOk {
60 affected: usize,
61 modified_catalog: bool,
62 },
63 Rows {
65 columns: Vec<ColumnSchema>,
66 rows: Vec<Row>,
67 },
68}
69
70#[derive(Debug, Clone, PartialEq)]
76#[non_exhaustive]
77pub enum EngineError {
78 Parse(ParseError),
79 Storage(StorageError),
80 Eval(EvalError),
81 Unsupported(String),
83 TransactionAlreadyOpen,
85 NoActiveTransaction,
87 WriteRequired,
92 RowLimitExceeded(usize),
95 Cancelled,
101}
102
103impl fmt::Display for EngineError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::Parse(e) => write!(f, "parse: {e}"),
107 Self::Storage(e) => write!(f, "storage: {e}"),
108 Self::Eval(e) => write!(f, "eval: {e}"),
109 Self::Unsupported(s) => write!(f, "unsupported: {s}"),
110 Self::TransactionAlreadyOpen => f.write_str("a transaction is already open"),
111 Self::NoActiveTransaction => f.write_str("no active transaction"),
112 Self::WriteRequired => {
113 f.write_str("statement requires a write lock (use execute, not execute_readonly)")
114 }
115 Self::RowLimitExceeded(n) => {
116 write!(f, "query exceeded max_query_rows={n}")
117 }
118 Self::Cancelled => f.write_str("query cancelled (timeout or client request)"),
119 }
120 }
121}
122
123impl From<ParseError> for EngineError {
124 fn from(e: ParseError) -> Self {
125 Self::Parse(e)
126 }
127}
128impl From<StorageError> for EngineError {
129 fn from(e: StorageError) -> Self {
130 Self::Storage(e)
131 }
132}
133impl From<EvalError> for EngineError {
134 fn from(e: EvalError) -> Self {
135 Self::Eval(e)
136 }
137}
138
139pub type ClockFn = fn() -> i64;
148
149pub type SaltFn = fn() -> [u8; 16];
156
157#[derive(Debug, Clone, Copy)]
168pub struct CancelToken<'a> {
169 flag: Option<&'a core::sync::atomic::AtomicBool>,
170}
171
172impl<'a> CancelToken<'a> {
173 #[must_use]
174 pub const fn none() -> Self {
175 Self { flag: None }
176 }
177
178 #[must_use]
179 pub const fn from_flag(f: &'a core::sync::atomic::AtomicBool) -> Self {
180 Self { flag: Some(f) }
181 }
182
183 #[must_use]
184 pub fn is_cancelled(self) -> bool {
185 self.flag
186 .is_some_and(|f| f.load(core::sync::atomic::Ordering::Relaxed))
187 }
188
189 #[inline]
193 pub fn check(self) -> Result<(), EngineError> {
194 if self.is_cancelled() {
195 Err(EngineError::Cancelled)
196 } else {
197 Ok(())
198 }
199 }
200}
201
202const ENVELOPE_MAGIC: &[u8; 8] = b"SPGENV01";
260const ENVELOPE_VERSION_V1: u8 = 1;
261const ENVELOPE_VERSION_V2: u8 = 2;
262const ENVELOPE_VERSION_V3: u8 = 3;
263const ENVELOPE_VERSION_V4: u8 = 4;
264const ENVELOPE_VERSION_V5: u8 = 5;
265
266fn build_envelope(
267 catalog: &[u8],
268 users: &[u8],
269 pubs: &[u8],
270 subs: &[u8],
271 stats: &[u8],
272) -> Vec<u8> {
273 let mut out = Vec::with_capacity(
274 8 + 1
275 + 4
276 + catalog.len()
277 + 4
278 + users.len()
279 + 4
280 + pubs.len()
281 + 4
282 + subs.len()
283 + 4
284 + stats.len()
285 + 4,
286 );
287 out.extend_from_slice(ENVELOPE_MAGIC);
288 out.push(ENVELOPE_VERSION_V5);
289 out.extend_from_slice(
290 &u32::try_from(catalog.len())
291 .expect("≤ 4G catalog")
292 .to_le_bytes(),
293 );
294 out.extend_from_slice(catalog);
295 out.extend_from_slice(
296 &u32::try_from(users.len())
297 .expect("≤ 4G users")
298 .to_le_bytes(),
299 );
300 out.extend_from_slice(users);
301 out.extend_from_slice(
302 &u32::try_from(pubs.len())
303 .expect("≤ 4G publications")
304 .to_le_bytes(),
305 );
306 out.extend_from_slice(pubs);
307 out.extend_from_slice(
308 &u32::try_from(subs.len())
309 .expect("≤ 4G subscriptions")
310 .to_le_bytes(),
311 );
312 out.extend_from_slice(subs);
313 out.extend_from_slice(
314 &u32::try_from(stats.len())
315 .expect("≤ 4G statistics")
316 .to_le_bytes(),
317 );
318 out.extend_from_slice(stats);
319 let crc = spg_crypto::crc32::crc32(&out);
320 out.extend_from_slice(&crc.to_le_bytes());
321 out
322}
323
324enum EnvelopeParse<'a> {
331 Bare,
332 Pair {
333 catalog: &'a [u8],
334 users: &'a [u8],
335 publications: Option<&'a [u8]>,
336 subscriptions: Option<&'a [u8]>,
337 statistics: Option<&'a [u8]>,
338 },
339 CrcMismatch {
340 expected: u32,
341 computed: u32,
342 },
343}
344
345fn split_envelope(buf: &[u8]) -> EnvelopeParse<'_> {
350 if buf.len() < 8 + 1 + 4 || &buf[..8] != ENVELOPE_MAGIC {
351 return EnvelopeParse::Bare;
352 }
353 let version = buf[8];
354 if !matches!(
355 version,
356 ENVELOPE_VERSION_V1
357 | ENVELOPE_VERSION_V2
358 | ENVELOPE_VERSION_V3
359 | ENVELOPE_VERSION_V4
360 | ENVELOPE_VERSION_V5
361 ) {
362 return EnvelopeParse::Bare;
363 }
364 let mut p = 9usize;
365 let Some(cat_len_bytes) = buf.get(p..p + 4) else {
366 return EnvelopeParse::Bare;
367 };
368 let Ok(cat_len_arr) = cat_len_bytes.try_into() else {
369 return EnvelopeParse::Bare;
370 };
371 let cat_len = u32::from_le_bytes(cat_len_arr) as usize;
372 p += 4;
373 if p + cat_len + 4 > buf.len() {
374 return EnvelopeParse::Bare;
375 }
376 let catalog = &buf[p..p + cat_len];
377 p += cat_len;
378 let Some(user_len_bytes) = buf.get(p..p + 4) else {
379 return EnvelopeParse::Bare;
380 };
381 let Ok(user_len_arr) = user_len_bytes.try_into() else {
382 return EnvelopeParse::Bare;
383 };
384 let user_len = u32::from_le_bytes(user_len_arr) as usize;
385 p += 4;
386 if p + user_len > buf.len() {
387 return EnvelopeParse::Bare;
388 }
389 let users = &buf[p..p + user_len];
390 p += user_len;
391 let publications = if matches!(
392 version,
393 ENVELOPE_VERSION_V3 | ENVELOPE_VERSION_V4 | ENVELOPE_VERSION_V5
394 ) {
395 let Some(pubs_len_bytes) = buf.get(p..p + 4) else {
397 return EnvelopeParse::Bare;
398 };
399 let Ok(pubs_len_arr) = pubs_len_bytes.try_into() else {
400 return EnvelopeParse::Bare;
401 };
402 let pubs_len = u32::from_le_bytes(pubs_len_arr) as usize;
403 p += 4;
404 if p + pubs_len > buf.len() {
405 return EnvelopeParse::Bare;
406 }
407 let pubs_slice = &buf[p..p + pubs_len];
408 p += pubs_len;
409 Some(pubs_slice)
410 } else {
411 None
412 };
413 let subscriptions = if matches!(version, ENVELOPE_VERSION_V4 | ENVELOPE_VERSION_V5) {
414 let Some(subs_len_bytes) = buf.get(p..p + 4) else {
416 return EnvelopeParse::Bare;
417 };
418 let Ok(subs_len_arr) = subs_len_bytes.try_into() else {
419 return EnvelopeParse::Bare;
420 };
421 let subs_len = u32::from_le_bytes(subs_len_arr) as usize;
422 p += 4;
423 if p + subs_len > buf.len() {
424 return EnvelopeParse::Bare;
425 }
426 let subs_slice = &buf[p..p + subs_len];
427 p += subs_len;
428 Some(subs_slice)
429 } else {
430 None
431 };
432 let statistics = if version == ENVELOPE_VERSION_V5 {
433 let Some(stats_len_bytes) = buf.get(p..p + 4) else {
435 return EnvelopeParse::Bare;
436 };
437 let Ok(stats_len_arr) = stats_len_bytes.try_into() else {
438 return EnvelopeParse::Bare;
439 };
440 let stats_len = u32::from_le_bytes(stats_len_arr) as usize;
441 p += 4;
442 if p + stats_len > buf.len() {
443 return EnvelopeParse::Bare;
444 }
445 let stats_slice = &buf[p..p + stats_len];
446 p += stats_len;
447 Some(stats_slice)
448 } else {
449 None
450 };
451 if matches!(
452 version,
453 ENVELOPE_VERSION_V2 | ENVELOPE_VERSION_V3 | ENVELOPE_VERSION_V4 | ENVELOPE_VERSION_V5
454 ) {
455 if p + 4 != buf.len() {
456 return EnvelopeParse::Bare;
457 }
458 let Ok(crc_arr) = buf[p..p + 4].try_into() else {
459 return EnvelopeParse::Bare;
460 };
461 let expected = u32::from_le_bytes(crc_arr);
462 let computed = spg_crypto::crc32::crc32(&buf[..p]);
463 if expected != computed {
464 return EnvelopeParse::CrcMismatch { expected, computed };
465 }
466 } else if p != buf.len() {
467 return EnvelopeParse::Bare;
469 }
470 EnvelopeParse::Pair {
471 catalog,
472 users,
473 publications,
474 subscriptions,
475 statistics,
476 }
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
489pub struct TxId(pub u64);
490
491pub const IMPLICIT_TX: TxId = TxId(0);
494
495pub const COMPACTION_TARGET_DEFAULT_BYTES: u64 = 4 * 1024 * 1024;
501
502#[derive(Debug, Default, Clone)]
507struct TxState {
508 catalog: Catalog,
513 savepoints: Vec<(String, Catalog)>,
519}
520
521#[derive(Debug, Default)]
522pub struct Engine {
523 catalog: Catalog,
526 tx_catalogs: BTreeMap<TxId, TxState>,
531 current_tx: Option<TxId>,
536 next_tx_id: u64,
539 clock: Option<ClockFn>,
542 salt_fn: Option<SaltFn>,
546 max_query_rows: Option<usize>,
552 users: UserStore,
558 publications: publications::Publications,
562 subscriptions: subscriptions::Subscriptions,
566 statistics: statistics::Statistics,
570 plan_cache: plan_cache::PlanCache,
574 query_stats: query_stats::QueryStats,
578 activity_provider: Option<ActivityProvider>,
585 audit_chain_provider: Option<AuditChainProvider>,
590 audit_verifier: Option<AuditVerifier>,
591 slow_query_threshold_us: Option<u64>,
597 slow_query_logger: Option<SlowQueryLogger>,
598}
599
600pub type SlowQueryLogger = fn(&str, u64);
604
605fn render_create_table(name: &str, columns: &[ColumnSchema]) -> String {
610 let mut out = alloc::format!("CREATE TABLE {name} (");
611 for (i, col) in columns.iter().enumerate() {
612 if i > 0 {
613 out.push_str(", ");
614 }
615 out.push_str(&col.name);
616 out.push(' ');
617 out.push_str(&render_data_type(col.ty));
618 if !col.nullable {
619 out.push_str(" NOT NULL");
620 }
621 if col.auto_increment {
622 out.push_str(" AUTO_INCREMENT");
623 }
624 }
625 out.push(')');
626 out
627}
628
629fn render_data_type(ty: DataType) -> String {
630 match ty {
631 DataType::SmallInt => "SMALLINT".into(),
632 DataType::Int => "INT".into(),
633 DataType::BigInt => "BIGINT".into(),
634 DataType::Float => "FLOAT".into(),
635 DataType::Text => "TEXT".into(),
636 DataType::Varchar(n) => alloc::format!("VARCHAR({n})"),
637 DataType::Char(n) => alloc::format!("CHAR({n})"),
638 DataType::Bool => "BOOL".into(),
639 DataType::Vector { dim, encoding } => match encoding {
640 spg_storage::VecEncoding::F32 => alloc::format!("VECTOR({dim})"),
641 spg_storage::VecEncoding::Sq8 => alloc::format!("VECTOR({dim}) USING SQ8"),
642 spg_storage::VecEncoding::F16 => alloc::format!("VECTOR({dim}) USING HALF"),
643 },
644 DataType::Numeric { precision, scale } => {
645 alloc::format!("NUMERIC({precision},{scale})")
646 }
647 DataType::Date => "DATE".into(),
648 DataType::Timestamp => "TIMESTAMP".into(),
649 DataType::Interval => "INTERVAL".into(),
650 DataType::Json => "JSON".into(),
651 DataType::Jsonb => "JSONB".into(),
652 DataType::Timestamptz => "TIMESTAMPTZ".into(),
653 }
654}
655
656#[derive(Debug, Clone)]
660pub struct ActivityRow {
661 pub pid: u32,
662 pub user: String,
663 pub started_at_us: i64,
664 pub current_sql: String,
665 pub wait_event: String,
666 pub elapsed_us: i64,
667 pub in_transaction: bool,
668}
669
670pub type ActivityProvider = fn() -> Vec<ActivityRow>;
673
674#[derive(Debug, Clone)]
677pub struct AuditRow {
678 pub seq: i64,
679 pub ts_ms: i64,
680 pub prev_hash_hex: String,
681 pub entry_hash_hex: String,
682 pub sql: String,
683}
684
685pub type AuditChainProvider = fn() -> Vec<AuditRow>;
690pub type AuditVerifier = fn() -> (i64, i64);
691
692impl Engine {
693 pub fn new() -> Self {
694 Self {
695 catalog: Catalog::new(),
696 tx_catalogs: BTreeMap::new(),
697 current_tx: None,
698 next_tx_id: 1,
699 clock: None,
700 salt_fn: None,
701 max_query_rows: None,
702 users: UserStore::new(),
703 publications: publications::Publications::new(),
704 subscriptions: subscriptions::Subscriptions::new(),
705 statistics: statistics::Statistics::new(),
706 plan_cache: plan_cache::PlanCache::new(),
707 query_stats: query_stats::QueryStats::new(),
708 activity_provider: None,
709 audit_chain_provider: None,
710 audit_verifier: None,
711 slow_query_threshold_us: None,
712 slow_query_logger: None,
713 }
714 }
715
716 pub fn restore(catalog: Catalog) -> Self {
719 Self {
720 catalog,
721 tx_catalogs: BTreeMap::new(),
722 current_tx: None,
723 next_tx_id: 1,
724 clock: None,
725 salt_fn: None,
726 max_query_rows: None,
727 users: UserStore::new(),
728 publications: publications::Publications::new(),
729 subscriptions: subscriptions::Subscriptions::new(),
730 statistics: statistics::Statistics::new(),
731 plan_cache: plan_cache::PlanCache::new(),
732 query_stats: query_stats::QueryStats::new(),
733 activity_provider: None,
734 audit_chain_provider: None,
735 audit_verifier: None,
736 slow_query_threshold_us: None,
737 slow_query_logger: None,
738 }
739 }
740
741 pub fn restore_envelope(buf: &[u8]) -> Result<Self, EngineError> {
748 match split_envelope(buf) {
749 EnvelopeParse::Pair {
750 catalog: catalog_bytes,
751 users: user_bytes,
752 publications: pub_bytes,
753 subscriptions: sub_bytes,
754 statistics: stats_bytes,
755 } => {
756 let catalog = Catalog::deserialize(catalog_bytes).map_err(EngineError::Storage)?;
757 let users = users::deserialize_users(user_bytes)
758 .map_err(|e| EngineError::Unsupported(alloc::format!("users restore: {e}")))?;
759 let publications = match pub_bytes {
760 Some(b) => publications::Publications::deserialize(b).map_err(|e| {
761 EngineError::Unsupported(alloc::format!("publications restore: {e:?}"))
762 })?,
763 None => publications::Publications::new(),
764 };
765 let subscriptions = match sub_bytes {
766 Some(b) => subscriptions::Subscriptions::deserialize(b).map_err(|e| {
767 EngineError::Unsupported(alloc::format!("subscriptions restore: {e:?}"))
768 })?,
769 None => subscriptions::Subscriptions::new(),
770 };
771 let statistics = match stats_bytes {
772 Some(b) => statistics::Statistics::deserialize(b).map_err(|e| {
773 EngineError::Unsupported(alloc::format!("statistics restore: {e:?}"))
774 })?,
775 None => statistics::Statistics::new(),
776 };
777 Ok(Self {
778 catalog,
779 tx_catalogs: BTreeMap::new(),
780 current_tx: None,
781 next_tx_id: 1,
782 clock: None,
783 salt_fn: None,
784 max_query_rows: None,
785 users,
786 publications,
787 subscriptions,
788 statistics,
789 plan_cache: plan_cache::PlanCache::new(),
790 query_stats: query_stats::QueryStats::new(),
791 activity_provider: None,
792 audit_chain_provider: None,
793 audit_verifier: None,
794 slow_query_threshold_us: None,
795 slow_query_logger: None,
796 })
797 }
798 EnvelopeParse::CrcMismatch { expected, computed } => {
799 Err(EngineError::Storage(StorageError::Corrupt(alloc::format!(
800 "snapshot envelope CRC32 mismatch (expected={expected:#010x}, computed={computed:#010x})"
801 ))))
802 }
803 EnvelopeParse::Bare => {
804 let catalog = Catalog::deserialize(buf).map_err(EngineError::Storage)?;
805 Ok(Self::restore(catalog))
806 }
807 }
808 }
809
810 pub const fn users(&self) -> &UserStore {
811 &self.users
812 }
813
814 pub fn create_user(
818 &mut self,
819 name: &str,
820 password: &str,
821 role: Role,
822 salt: [u8; 16],
823 ) -> Result<(), UserError> {
824 self.users.create(name, password, role, salt)?;
825 let scram_salt = self.salt_fn.map_or_else(
831 || {
832 let mut s = [0u8; users::SCRAM_SALT_LEN];
833 let digest = spg_crypto::hash(name.as_bytes());
834 s.copy_from_slice(&digest[16..32]);
837 s
838 },
839 |f| f(),
840 );
841 self.users
842 .enable_scram(name, password, scram_salt, users::SCRAM_DEFAULT_ITERS)?;
843 Ok(())
844 }
845
846 pub fn drop_user(&mut self, name: &str) -> Result<(), UserError> {
847 self.users.drop(name)
848 }
849
850 pub fn verify_user(&self, name: &str, password: &str) -> Option<Role> {
851 self.users.verify(name, password)
852 }
853
854 #[must_use]
857 pub const fn with_clock(mut self, clock: ClockFn) -> Self {
858 self.clock = Some(clock);
859 self
860 }
861
862 #[must_use]
865 pub const fn with_salt_fn(mut self, f: SaltFn) -> Self {
866 self.salt_fn = Some(f);
867 self
868 }
869
870 #[must_use]
876 pub const fn with_max_query_rows(mut self, n: usize) -> Self {
877 self.max_query_rows = Some(n);
878 self
879 }
880
881 pub const fn catalog(&self) -> &Catalog {
885 &self.catalog
886 }
887
888 pub fn snapshot(&self) -> Vec<u8> {
896 if self.users.is_empty()
897 && self.publications.is_empty()
898 && self.subscriptions.is_empty()
899 && self.statistics.is_empty()
900 {
901 self.catalog.serialize()
902 } else {
903 build_envelope(
904 &self.catalog.serialize(),
905 &users::serialize_users(&self.users),
906 &self.publications.serialize(),
907 &self.subscriptions.serialize(),
908 &self.statistics.serialize(),
909 )
910 }
911 }
912
913 pub fn in_transaction(&self) -> bool {
918 !self.tx_catalogs.is_empty()
919 }
920
921 pub fn alloc_tx_id(&mut self) -> TxId {
930 let id = TxId(self.next_tx_id);
931 self.next_tx_id = self.next_tx_id.saturating_add(1);
932 id
933 }
934
935 pub fn replace_catalog(&mut self, catalog: Catalog) {
955 self.catalog = catalog;
956 }
957
958 pub fn freeze_oldest_to_cold(
966 &mut self,
967 table_name: &str,
968 index_name: &str,
969 max_rows: usize,
970 ) -> Result<spg_storage::FreezeReport, EngineError> {
971 let report = self
972 .active_catalog_mut()
973 .freeze_oldest_to_cold(table_name, index_name, max_rows)
974 .map_err(EngineError::Storage)?;
975 if let Some(t) = self.active_catalog_mut().get_mut(table_name) {
976 t.mark_cold_row_count_stale();
977 }
978 Ok(report)
979 }
980
981 pub fn receive_cold_segment(
995 &mut self,
996 segment_id: u32,
997 bytes: Vec<u8>,
998 ) -> Result<(), EngineError> {
999 let mut new_cat = self.catalog.clone();
1000 match new_cat.load_segment_bytes_at(segment_id, bytes) {
1001 Ok(()) => {
1002 self.replace_catalog(new_cat);
1003 Ok(())
1004 }
1005 Err(StorageError::Corrupt(msg)) if msg.contains("already occupied") => Ok(()),
1006 Err(e) => Err(EngineError::Storage(e)),
1007 }
1008 }
1009
1010 pub fn compact_cold_segments_with_target(
1024 &mut self,
1025 target_segment_bytes: u64,
1026 ) -> Result<Vec<(String, String, CompactReport)>, EngineError> {
1027 let table_names = self.active_catalog().table_names();
1028 let mut reports: Vec<(String, String, CompactReport)> = Vec::new();
1029 for tname in table_names {
1030 if is_internal_table_name(&tname) {
1031 continue;
1032 }
1033 let idx_names: Vec<String> = {
1034 let Some(t) = self.active_catalog().get(&tname) else {
1035 continue;
1036 };
1037 t.indices()
1038 .iter()
1039 .filter(|i| matches!(i.kind, IndexKind::BTree(_)))
1040 .map(|i| i.name.clone())
1041 .collect()
1042 };
1043 for iname in idx_names {
1044 let report = self
1045 .active_catalog_mut()
1046 .compact_cold_segments(&tname, &iname, target_segment_bytes)
1047 .map_err(EngineError::Storage)?;
1048 if report.merged_segment_id.is_some() {
1049 if let Some(t) = self.active_catalog_mut().get_mut(&tname) {
1050 t.mark_cold_row_count_stale();
1051 }
1052 reports.push((tname.clone(), iname, report));
1053 }
1054 }
1055 }
1056 Ok(reports)
1057 }
1058
1059 fn active_catalog(&self) -> &Catalog {
1060 match self.current_tx {
1061 Some(t) => self
1062 .tx_catalogs
1063 .get(&t)
1064 .map_or(&self.catalog, |s| &s.catalog),
1065 None => &self.catalog,
1066 }
1067 }
1068
1069 fn active_catalog_mut(&mut self) -> &mut Catalog {
1070 let tx = self.current_tx;
1071 match tx {
1072 Some(t) => match self.tx_catalogs.get_mut(&t) {
1073 Some(s) => &mut s.catalog,
1074 None => &mut self.catalog,
1075 },
1076 None => &mut self.catalog,
1077 }
1078 }
1079
1080 pub fn execute_readonly(&self, sql: &str) -> Result<QueryResult, EngineError> {
1092 self.execute_readonly_with_cancel(sql, CancelToken::none())
1093 }
1094
1095 pub fn execute_readonly_with_cancel(
1101 &self,
1102 sql: &str,
1103 cancel: CancelToken<'_>,
1104 ) -> Result<QueryResult, EngineError> {
1105 cancel.check()?;
1106 let mut stmt = parser::parse_statement(sql)?;
1107 let now_micros = self.clock.map(|f| f());
1108 rewrite_clock_calls(&mut stmt, now_micros);
1109 if let Statement::Select(s) = &mut stmt {
1110 resolve_order_by_position(s);
1111 reorder::reorder_joins(s, &self.catalog, &self.statistics);
1113 }
1114 let result = match stmt {
1115 Statement::Select(s) => self.exec_select_cancel(&s, cancel),
1116 Statement::ShowTables => Ok(self.exec_show_tables()),
1117 Statement::ShowColumns(table) => self.exec_show_columns(&table),
1118 Statement::ShowUsers => Ok(self.exec_show_users()),
1119 Statement::ShowPublications => Ok(self.exec_show_publications()),
1120 Statement::ShowSubscriptions => Ok(self.exec_show_subscriptions()),
1121 Statement::WaitForWalPosition { .. } => Err(EngineError::Unsupported(
1122 "WAIT FOR WAL POSITION must be handled by the server layer".into(),
1123 )),
1124 Statement::Explain(e) => self.exec_explain(&e, cancel),
1125 _ => Err(EngineError::WriteRequired),
1126 };
1127 self.enforce_row_limit(result)
1128 }
1129
1130 fn enforce_row_limit(
1134 &self,
1135 result: Result<QueryResult, EngineError>,
1136 ) -> Result<QueryResult, EngineError> {
1137 if let (Ok(QueryResult::Rows { rows, .. }), Some(cap)) = (&result, self.max_query_rows)
1138 && rows.len() > cap
1139 {
1140 return Err(EngineError::RowLimitExceeded(cap));
1141 }
1142 result
1143 }
1144
1145 pub fn execute(&mut self, sql: &str) -> Result<QueryResult, EngineError> {
1146 self.execute_in_with_cancel(sql, IMPLICIT_TX, CancelToken::none())
1147 }
1148
1149 pub fn execute_with_cancel(
1154 &mut self,
1155 sql: &str,
1156 cancel: CancelToken<'_>,
1157 ) -> Result<QueryResult, EngineError> {
1158 self.execute_in_with_cancel(sql, IMPLICIT_TX, cancel)
1159 }
1160
1161 pub fn execute_in(&mut self, sql: &str, tx_id: TxId) -> Result<QueryResult, EngineError> {
1168 self.execute_in_with_cancel(sql, tx_id, CancelToken::none())
1169 }
1170
1171 pub fn execute_in_with_cancel(
1177 &mut self,
1178 sql: &str,
1179 tx_id: TxId,
1180 cancel: CancelToken<'_>,
1181 ) -> Result<QueryResult, EngineError> {
1182 let saved = self.current_tx;
1183 self.current_tx = Some(tx_id);
1184 let result = self.execute_inner_with_cancel(sql, cancel);
1185 self.current_tx = saved;
1186 result
1187 }
1188
1189 pub fn prepare(&self, sql: &str) -> Result<Statement, ParseError> {
1201 let mut stmt = parser::parse_statement(sql)?;
1202 let now_micros = self.clock.map(|f| f());
1203 rewrite_clock_calls(&mut stmt, now_micros);
1204 if let Statement::Select(s) = &mut stmt {
1205 expand_group_by_all(s);
1209 resolve_order_by_position(s);
1210 reorder::reorder_joins(s, &self.catalog, &self.statistics);
1213 }
1214 Ok(stmt)
1215 }
1216
1217 pub fn prepare_cached(&mut self, sql: &str) -> Result<Statement, ParseError> {
1229 let current_version = self.statistics.version();
1232 if let Some(plan) = self.plan_cache.get(sql) {
1233 if plan.statistics_version == current_version {
1234 return Ok(plan.stmt.clone());
1235 }
1236 }
1238 self.plan_cache.evict(sql);
1239 let stmt = self.prepare(sql)?;
1240 let source_tables = plan_cache::collect_source_tables(&stmt);
1241 let plan = plan_cache::PreparedPlan {
1242 stmt: stmt.clone(),
1243 statistics_version: current_version,
1244 source_tables,
1245 describe_columns: alloc::vec::Vec::new(),
1246 };
1247 self.plan_cache.insert(String::from(sql), plan);
1248 Ok(stmt)
1249 }
1250
1251 pub fn plan_cache(&self) -> &plan_cache::PlanCache {
1253 &self.plan_cache
1254 }
1255
1256 pub fn plan_cache_mut(&mut self) -> &mut plan_cache::PlanCache {
1258 &mut self.plan_cache
1259 }
1260
1261 pub fn describe_prepared(
1267 &self,
1268 stmt: &Statement,
1269 ) -> (Vec<u32>, Vec<ColumnSchema>) {
1270 describe::describe_prepared(stmt, self.active_catalog())
1271 }
1272
1273 pub fn execute_prepared(
1283 &mut self,
1284 mut stmt: Statement,
1285 params: &[Value],
1286 ) -> Result<QueryResult, EngineError> {
1287 substitute_placeholders(&mut stmt, params)?;
1288 self.execute_stmt_with_cancel(stmt, CancelToken::none())
1289 }
1290
1291 fn execute_inner_with_cancel(
1292 &mut self,
1293 sql: &str,
1294 cancel: CancelToken<'_>,
1295 ) -> Result<QueryResult, EngineError> {
1296 cancel.check()?;
1297 let stmt = self.prepare(sql)?;
1298 let start_us = self.clock.map(|f| f());
1302 let result = self.execute_stmt_with_cancel(stmt, cancel);
1303 if let (Some(t0), Ok(_)) = (start_us, &result) {
1304 let now = self.clock.map_or(t0, |f| f());
1305 let elapsed = now.saturating_sub(t0).max(0) as u64;
1306 self.query_stats.record(sql, elapsed, now as u64);
1307 if let (Some(threshold), Some(logger)) =
1310 (self.slow_query_threshold_us, self.slow_query_logger)
1311 && elapsed >= threshold
1312 {
1313 logger(sql, elapsed);
1314 }
1315 }
1316 result
1317 }
1318
1319 fn execute_stmt_with_cancel(
1320 &mut self,
1321 stmt: Statement,
1322 cancel: CancelToken<'_>,
1323 ) -> Result<QueryResult, EngineError> {
1324 cancel.check()?;
1325 let result = match stmt {
1326 Statement::CreateTable(s) => self.exec_create_table(s),
1327 Statement::CreateExtension(_) => Ok(QueryResult::CommandOk {
1331 affected: 0,
1332 modified_catalog: false,
1333 }),
1334 Statement::CreateIndex(s) => self.exec_create_index(s),
1335 Statement::Insert(s) => self.exec_insert(s),
1336 Statement::Update(s) => self.exec_update_cancel(&s, cancel),
1337 Statement::Delete(s) => self.exec_delete_cancel(&s, cancel),
1338 Statement::Select(s) => self.exec_select_cancel(&s, cancel),
1339 Statement::Begin => self.exec_begin(),
1340 Statement::Commit => self.exec_commit(),
1341 Statement::Rollback => self.exec_rollback(),
1342 Statement::Savepoint(name) => self.exec_savepoint(name),
1343 Statement::RollbackToSavepoint(name) => self.exec_rollback_to_savepoint(&name),
1344 Statement::ReleaseSavepoint(name) => self.exec_release_savepoint(&name),
1345 Statement::ShowTables => Ok(self.exec_show_tables()),
1346 Statement::ShowColumns(table) => self.exec_show_columns(&table),
1347 Statement::ShowUsers => Ok(self.exec_show_users()),
1348 Statement::ShowPublications => Ok(self.exec_show_publications()),
1349 Statement::ShowSubscriptions => Ok(self.exec_show_subscriptions()),
1350 Statement::CreateUser(s) => self.exec_create_user(&s),
1351 Statement::DropUser(name) => self.exec_drop_user(&name),
1352 Statement::Explain(e) => self.exec_explain(&e, cancel),
1353 Statement::AlterIndex(s) => self.exec_alter_index(s),
1354 Statement::AlterTable(s) => self.exec_alter_table(s),
1355 Statement::CreatePublication(s) => self.exec_create_publication(s),
1356 Statement::DropPublication(name) => self.exec_drop_publication(&name),
1357 Statement::CreateSubscription(s) => self.exec_create_subscription(s),
1358 Statement::DropSubscription(name) => self.exec_drop_subscription(&name),
1359 Statement::WaitForWalPosition { .. } => Err(EngineError::Unsupported(
1366 "WAIT FOR WAL POSITION must be handled by the server layer".into(),
1367 )),
1368 Statement::Analyze(target) => self.exec_analyze(target.as_deref()),
1370 Statement::CompactColdSegments => self.exec_compact_cold_segments(),
1372 };
1373 self.enforce_row_limit(result)
1374 }
1375
1376 fn exec_create_publication(
1384 &mut self,
1385 s: CreatePublicationStatement,
1386 ) -> Result<QueryResult, EngineError> {
1387 self.publications
1393 .create(s.name, s.scope)
1394 .map_err(|e| EngineError::Unsupported(alloc::format!("CREATE PUBLICATION: {e:?}")))?;
1395 Ok(QueryResult::CommandOk {
1396 affected: 1,
1397 modified_catalog: true,
1398 })
1399 }
1400
1401 fn exec_drop_publication(&mut self, name: &str) -> Result<QueryResult, EngineError> {
1406 let removed = self.publications.drop(name);
1407 Ok(QueryResult::CommandOk {
1408 affected: usize::from(removed),
1409 modified_catalog: removed,
1410 })
1411 }
1412
1413 pub const fn publications(&self) -> &publications::Publications {
1418 &self.publications
1419 }
1420
1421 fn exec_create_subscription(
1426 &mut self,
1427 s: CreateSubscriptionStatement,
1428 ) -> Result<QueryResult, EngineError> {
1429 let sub = subscriptions::Subscription {
1433 conn_str: s.conn_str,
1434 publications: s.publications,
1435 enabled: true,
1436 last_received_pos: 0,
1437 };
1438 self.subscriptions
1439 .create(s.name, sub)
1440 .map_err(|e| EngineError::Unsupported(alloc::format!("CREATE SUBSCRIPTION: {e:?}")))?;
1441 Ok(QueryResult::CommandOk {
1442 affected: 1,
1443 modified_catalog: true,
1444 })
1445 }
1446
1447 fn exec_drop_subscription(&mut self, name: &str) -> Result<QueryResult, EngineError> {
1455 let removed = self.subscriptions.drop(name);
1456 Ok(QueryResult::CommandOk {
1457 affected: usize::from(removed),
1458 modified_catalog: removed,
1459 })
1460 }
1461
1462 pub const fn subscriptions(&self) -> &subscriptions::Subscriptions {
1467 &self.subscriptions
1468 }
1469
1470 pub fn subscription_advance(&mut self, name: &str, pos: u64) -> bool {
1476 self.subscriptions.update_last_received_pos(name, pos)
1477 }
1478
1479 fn exec_show_subscriptions(&self) -> QueryResult {
1485 let columns = alloc::vec![
1486 ColumnSchema::new("name", DataType::Text, false),
1487 ColumnSchema::new("conn_str", DataType::Text, false),
1488 ColumnSchema::new("publications", DataType::Text, false),
1489 ColumnSchema::new("enabled", DataType::Bool, false),
1490 ColumnSchema::new("last_received_pos", DataType::BigInt, false),
1491 ];
1492 let rows: Vec<Row> = self
1493 .subscriptions
1494 .iter()
1495 .map(|(name, sub)| {
1496 Row::new(alloc::vec![
1497 Value::Text(name.clone()),
1498 Value::Text(sub.conn_str.clone()),
1499 Value::Text(sub.publications.join(", ")),
1500 Value::Bool(sub.enabled),
1501 Value::BigInt(i64::try_from(sub.last_received_pos).unwrap_or(i64::MAX)),
1502 ])
1503 })
1504 .collect();
1505 QueryResult::Rows { columns, rows }
1506 }
1507
1508 fn exec_spg_statistic(&self) -> QueryResult {
1513 let columns = alloc::vec![
1514 ColumnSchema::new("table_name", DataType::Text, false),
1515 ColumnSchema::new("column_name", DataType::Text, false),
1516 ColumnSchema::new("null_frac", DataType::Float, false),
1517 ColumnSchema::new("n_distinct", DataType::BigInt, false),
1518 ColumnSchema::new("histogram_bounds", DataType::Text, false),
1519 ColumnSchema::new("cold_row_count", DataType::BigInt, false),
1524 ];
1525 let rows: Vec<Row> = self
1526 .statistics
1527 .iter()
1528 .map(|((t, c), s)| {
1529 let cold = self
1530 .catalog
1531 .get(t)
1532 .map_or(0, |table| table.cold_row_count());
1533 Row::new(alloc::vec![
1534 Value::Text(t.clone()),
1535 Value::Text(c.clone()),
1536 Value::Float(f64::from(s.null_frac)),
1537 Value::BigInt(i64::try_from(s.n_distinct).unwrap_or(i64::MAX)),
1538 Value::Text(render_histogram_bounds(&s.histogram_bounds)),
1539 Value::BigInt(i64::try_from(cold).unwrap_or(i64::MAX)),
1540 ])
1541 })
1542 .collect();
1543 QueryResult::Rows { columns, rows }
1544 }
1545
1546 fn exec_spg_stat_replication(&self) -> QueryResult {
1553 let columns = alloc::vec![
1554 ColumnSchema::new("name", DataType::Text, false),
1555 ColumnSchema::new("conn_str", DataType::Text, false),
1556 ColumnSchema::new("publications", DataType::Text, false),
1557 ColumnSchema::new("last_received_pos", DataType::BigInt, false),
1558 ColumnSchema::new("enabled", DataType::Bool, false),
1559 ];
1560 let rows: Vec<Row> = self
1561 .subscriptions
1562 .iter()
1563 .map(|(name, sub)| {
1564 Row::new(alloc::vec![
1565 Value::Text(name.clone()),
1566 Value::Text(sub.conn_str.clone()),
1567 Value::Text(sub.publications.join(",")),
1568 Value::BigInt(i64::try_from(sub.last_received_pos).unwrap_or(i64::MAX)),
1569 Value::Bool(sub.enabled),
1570 ])
1571 })
1572 .collect();
1573 QueryResult::Rows { columns, rows }
1574 }
1575
1576 fn exec_spg_stat_segment(&self) -> QueryResult {
1588 let columns = alloc::vec![
1589 ColumnSchema::new("segment_id", DataType::BigInt, false),
1590 ColumnSchema::new("table_name", DataType::Text, false),
1591 ColumnSchema::new("num_rows", DataType::BigInt, false),
1592 ColumnSchema::new("num_pages", DataType::BigInt, false),
1593 ColumnSchema::new("total_bytes", DataType::BigInt, false),
1594 ];
1595 let mut segment_owners: alloc::collections::BTreeMap<u32, String> = BTreeMap::new();
1601 for tname in self.catalog.table_names() {
1602 if is_internal_table_name(&tname) {
1603 continue;
1604 }
1605 let Some(t) = self.catalog.get(&tname) else {
1606 continue;
1607 };
1608 for idx in t.indices() {
1609 if let spg_storage::IndexKind::BTree(map) = &idx.kind {
1610 for (_, locs) in map.iter() {
1611 for loc in locs {
1612 if let spg_storage::RowLocator::Cold { segment_id, .. } = loc {
1613 segment_owners.entry(*segment_id).or_insert_with(|| tname.clone());
1614 }
1615 }
1616 }
1617 }
1618 }
1619 }
1620 let rows: Vec<Row> = self
1621 .catalog
1622 .cold_segment_ids_global()
1623 .iter()
1624 .filter_map(|&id| {
1625 let seg = self.catalog.cold_segment(id)?;
1626 let meta = seg.meta();
1627 let owner = segment_owners
1628 .get(&id)
1629 .cloned()
1630 .unwrap_or_default();
1631 Some(Row::new(alloc::vec![
1632 Value::BigInt(i64::from(id)),
1633 Value::Text(owner),
1634 Value::BigInt(i64::try_from(meta.num_rows).unwrap_or(i64::MAX)),
1635 Value::BigInt(i64::from(meta.num_pages)),
1636 Value::BigInt(i64::try_from(meta.total_bytes).unwrap_or(i64::MAX)),
1637 ]))
1638 })
1639 .collect();
1640 QueryResult::Rows { columns, rows }
1641 }
1642
1643 fn exec_spg_stat_query(&self) -> QueryResult {
1649 let columns = alloc::vec![
1650 ColumnSchema::new("sql", DataType::Text, false),
1651 ColumnSchema::new("exec_count", DataType::BigInt, false),
1652 ColumnSchema::new("total_us", DataType::BigInt, false),
1653 ColumnSchema::new("mean_us", DataType::BigInt, false),
1654 ColumnSchema::new("max_us", DataType::BigInt, false),
1655 ColumnSchema::new("last_seen_us", DataType::BigInt, false),
1656 ];
1657 let rows: Vec<Row> = self
1658 .query_stats
1659 .snapshot()
1660 .into_iter()
1661 .map(|(sql, s)| {
1662 let mean = if s.exec_count == 0 {
1663 0
1664 } else {
1665 s.total_us / s.exec_count
1666 };
1667 Row::new(alloc::vec![
1668 Value::Text(sql),
1669 Value::BigInt(i64::try_from(s.exec_count).unwrap_or(i64::MAX)),
1670 Value::BigInt(i64::try_from(s.total_us).unwrap_or(i64::MAX)),
1671 Value::BigInt(i64::try_from(mean).unwrap_or(i64::MAX)),
1672 Value::BigInt(i64::try_from(s.max_us).unwrap_or(i64::MAX)),
1673 Value::BigInt(i64::try_from(s.last_seen_us).unwrap_or(i64::MAX)),
1674 ])
1675 })
1676 .collect();
1677 QueryResult::Rows { columns, rows }
1678 }
1679
1680 #[must_use]
1685 pub const fn with_activity_provider(mut self, f: ActivityProvider) -> Self {
1686 self.activity_provider = Some(f);
1687 self
1688 }
1689
1690 #[must_use]
1692 pub const fn with_audit_providers(
1693 mut self,
1694 chain: AuditChainProvider,
1695 verify: AuditVerifier,
1696 ) -> Self {
1697 self.audit_chain_provider = Some(chain);
1698 self.audit_verifier = Some(verify);
1699 self
1700 }
1701
1702 #[must_use]
1707 pub const fn with_slow_query_log(
1708 mut self,
1709 threshold_us: u64,
1710 logger: SlowQueryLogger,
1711 ) -> Self {
1712 self.slow_query_threshold_us = Some(threshold_us);
1713 self.slow_query_logger = Some(logger);
1714 self
1715 }
1716
1717 pub fn set_plan_cache_max(&mut self, n: usize) {
1721 self.plan_cache.set_max_entries(n);
1722 }
1723
1724 fn exec_spg_stat_activity(&self) -> QueryResult {
1729 let columns = alloc::vec![
1730 ColumnSchema::new("pid", DataType::Int, false),
1731 ColumnSchema::new("user", DataType::Text, false),
1732 ColumnSchema::new("started_at_us", DataType::BigInt, false),
1733 ColumnSchema::new("current_sql", DataType::Text, false),
1734 ColumnSchema::new("wait_event", DataType::Text, false),
1735 ColumnSchema::new("elapsed_us", DataType::BigInt, false),
1736 ColumnSchema::new("in_transaction", DataType::Bool, false),
1737 ];
1738 let rows: Vec<Row> = self
1739 .activity_provider
1740 .map(|f| f())
1741 .unwrap_or_default()
1742 .into_iter()
1743 .map(|r| {
1744 Row::new(alloc::vec![
1745 Value::Int(i32::try_from(r.pid).unwrap_or(i32::MAX)),
1746 Value::Text(r.user),
1747 Value::BigInt(r.started_at_us),
1748 Value::Text(r.current_sql),
1749 Value::Text(r.wait_event),
1750 Value::BigInt(r.elapsed_us),
1751 Value::Bool(r.in_transaction),
1752 ])
1753 })
1754 .collect();
1755 QueryResult::Rows { columns, rows }
1756 }
1757
1758 fn exec_spg_table_ddl(&self) -> QueryResult {
1762 let columns = alloc::vec![
1763 ColumnSchema::new("table_name", DataType::Text, false),
1764 ColumnSchema::new("ddl", DataType::Text, false),
1765 ];
1766 let rows: Vec<Row> = self
1767 .catalog
1768 .table_names()
1769 .into_iter()
1770 .filter(|n| !is_internal_table_name(n))
1771 .filter_map(|name| {
1772 let table = self.catalog.get(&name)?;
1773 let ddl = render_create_table(&name, &table.schema().columns);
1774 Some(Row::new(alloc::vec![
1775 Value::Text(name),
1776 Value::Text(ddl),
1777 ]))
1778 })
1779 .collect();
1780 QueryResult::Rows { columns, rows }
1781 }
1782
1783 fn exec_spg_role_ddl(&self) -> QueryResult {
1787 let columns = alloc::vec![
1788 ColumnSchema::new("role_name", DataType::Text, false),
1789 ColumnSchema::new("ddl", DataType::Text, false),
1790 ];
1791 let rows: Vec<Row> = self
1792 .users
1793 .iter()
1794 .map(|(name, rec)| {
1795 let ddl = alloc::format!(
1796 "CREATE USER {name} WITH PASSWORD '<redacted>' ROLE '{}'",
1797 rec.role.as_str(),
1798 );
1799 Row::new(alloc::vec![Value::Text(String::from(name)), Value::Text(ddl)])
1800 })
1801 .collect();
1802 QueryResult::Rows { columns, rows }
1803 }
1804
1805 fn exec_spg_database_ddl(&self) -> QueryResult {
1811 let columns = alloc::vec![ColumnSchema::new("ddl", DataType::Text, false)];
1812 let mut out = String::new();
1813 for (name, rec) in self.users.iter() {
1814 out.push_str(&alloc::format!(
1815 "CREATE USER {name} WITH PASSWORD '<redacted>' ROLE '{}';\n",
1816 rec.role.as_str(),
1817 ));
1818 }
1819 for name in self.catalog.table_names() {
1820 if is_internal_table_name(&name) {
1821 continue;
1822 }
1823 if let Some(table) = self.catalog.get(&name) {
1824 out.push_str(&render_create_table(&name, &table.schema().columns));
1825 out.push_str(";\n");
1826 }
1827 }
1828 QueryResult::Rows {
1829 columns,
1830 rows: alloc::vec![Row::new(alloc::vec![Value::Text(out)])],
1831 }
1832 }
1833
1834 fn exec_spg_audit_chain(&self) -> QueryResult {
1838 let columns = alloc::vec![
1839 ColumnSchema::new("seq", DataType::BigInt, false),
1840 ColumnSchema::new("ts_ms", DataType::BigInt, false),
1841 ColumnSchema::new("prev_hash", DataType::Text, false),
1842 ColumnSchema::new("entry_hash", DataType::Text, false),
1843 ColumnSchema::new("sql", DataType::Text, false),
1844 ];
1845 let rows: Vec<Row> = self
1846 .audit_chain_provider
1847 .map(|f| f())
1848 .unwrap_or_default()
1849 .into_iter()
1850 .map(|r| {
1851 Row::new(alloc::vec![
1852 Value::BigInt(r.seq),
1853 Value::BigInt(r.ts_ms),
1854 Value::Text(r.prev_hash_hex),
1855 Value::Text(r.entry_hash_hex),
1856 Value::Text(r.sql),
1857 ])
1858 })
1859 .collect();
1860 QueryResult::Rows { columns, rows }
1861 }
1862
1863 fn exec_spg_audit_verify(&self) -> QueryResult {
1869 let columns = alloc::vec![
1870 ColumnSchema::new("verified_count", DataType::BigInt, false),
1871 ColumnSchema::new("broken_at_seq", DataType::BigInt, false),
1872 ];
1873 let (verified, broken) = self.audit_verifier.map(|f| f()).unwrap_or((0, -1));
1874 let row = Row::new(alloc::vec![
1875 Value::BigInt(verified),
1876 Value::BigInt(broken),
1877 ]);
1878 QueryResult::Rows {
1879 columns,
1880 rows: alloc::vec![row],
1881 }
1882 }
1883
1884 pub fn query_stats(&self) -> &query_stats::QueryStats {
1886 &self.query_stats
1887 }
1888
1889 pub fn query_stats_mut(&mut self) -> &mut query_stats::QueryStats {
1891 &mut self.query_stats
1892 }
1893
1894 pub const fn statistics(&self) -> &statistics::Statistics {
1898 &self.statistics
1899 }
1900
1901 pub fn tables_needing_analyze(&self) -> Vec<String> {
1914 const MIN_ROWS: u64 = 100;
1915 let mut out = Vec::new();
1916 for name in self.catalog.table_names() {
1917 if is_internal_table_name(&name) {
1918 continue;
1919 }
1920 let Some(table) = self.catalog.get(&name) else {
1921 continue;
1922 };
1923 let row_count = table.rows().len() as u64;
1924 let modified = self.statistics.modified_since_last_analyze(&name);
1925 let base = row_count.max(MIN_ROWS);
1930 let threshold = base.saturating_add(9) / 10;
1931 if modified >= threshold {
1932 out.push(name);
1933 }
1934 }
1935 out
1936 }
1937
1938 fn exec_analyze(&mut self, target: Option<&str>) -> Result<QueryResult, EngineError> {
1949 let names: Vec<String> = if let Some(name) = target {
1950 if self.catalog.get(name).is_none() {
1952 return Err(EngineError::Storage(StorageError::TableNotFound {
1953 name: name.to_string(),
1954 }));
1955 }
1956 alloc::vec![name.to_string()]
1957 } else {
1958 self.catalog
1959 .table_names()
1960 .into_iter()
1961 .filter(|n| !is_internal_table_name(n))
1962 .collect()
1963 };
1964 let mut analysed = 0usize;
1965 for table_name in &names {
1966 self.analyze_one_table(table_name)?;
1967 analysed += 1;
1968 }
1969 if analysed > 0 {
1975 self.statistics.bump_version();
1976 if target.is_some() {
1977 for t in &names {
1978 self.plan_cache.evict_referencing(t);
1979 }
1980 } else {
1981 self.plan_cache.clear();
1982 }
1983 }
1984 Ok(QueryResult::CommandOk {
1985 affected: analysed,
1986 modified_catalog: true,
1987 })
1988 }
1989
1990 fn exec_compact_cold_segments(&mut self) -> Result<QueryResult, EngineError> {
2001 let target = COMPACTION_TARGET_DEFAULT_BYTES;
2002 let reports = self.compact_cold_segments_with_target(target)?;
2003 let columns = alloc::vec![
2004 ColumnSchema::new("table_name", DataType::Text, false),
2005 ColumnSchema::new("index_name", DataType::Text, false),
2006 ColumnSchema::new("sources_merged", DataType::BigInt, false),
2007 ColumnSchema::new("merged_segment_id", DataType::BigInt, false),
2008 ColumnSchema::new("merged_rows", DataType::BigInt, false),
2009 ColumnSchema::new("deleted_rows_pruned", DataType::BigInt, false),
2010 ColumnSchema::new("bytes_reclaimed_estimate", DataType::BigInt, false),
2011 ];
2012 let rows: Vec<Row> = reports
2013 .into_iter()
2014 .map(|(tname, iname, report)| {
2015 Row::new(alloc::vec![
2016 Value::Text(tname),
2017 Value::Text(iname),
2018 Value::BigInt(i64::try_from(report.sources.len()).unwrap_or(i64::MAX)),
2019 Value::BigInt(i64::from(report.merged_segment_id.unwrap_or(0))),
2020 Value::BigInt(i64::try_from(report.merged_rows).unwrap_or(i64::MAX)),
2021 Value::BigInt(
2022 i64::try_from(report.deleted_rows_pruned).unwrap_or(i64::MAX),
2023 ),
2024 Value::BigInt(
2025 i64::try_from(report.bytes_reclaimed_estimate).unwrap_or(i64::MAX),
2026 ),
2027 ])
2028 })
2029 .collect();
2030 Ok(QueryResult::Rows { columns, rows })
2031 }
2032
2033 fn analyze_one_table(&mut self, table_name: &str) -> Result<(), EngineError> {
2038 let table = self.catalog.get(table_name).ok_or_else(|| {
2039 EngineError::Storage(StorageError::TableNotFound {
2040 name: table_name.to_string(),
2041 })
2042 })?;
2043 let schema = table.schema().clone();
2044 let row_count = table.rows().len();
2045 self.statistics.clear_table(table_name);
2050 for (col_pos, col_schema) in schema.columns.iter().enumerate() {
2051 if matches!(col_schema.ty, DataType::Vector { .. }) {
2054 continue;
2055 }
2056 let mut non_null_values: Vec<Value> = Vec::with_capacity(row_count);
2057 let mut nulls: u64 = 0;
2058 for row in table.rows() {
2059 match row.values.get(col_pos) {
2060 Some(Value::Null) | None => nulls += 1,
2061 Some(v) => non_null_values.push(v.clone()),
2062 }
2063 }
2064 non_null_values.sort_by(|a, b| sort_values_for_histogram(a, b));
2069 let non_null: Vec<String> = non_null_values
2070 .iter()
2071 .map(canonical_value_repr)
2072 .collect();
2073 let null_frac = if row_count == 0 {
2074 0.0
2075 } else {
2076 #[allow(clippy::cast_precision_loss)]
2077 let f = nulls as f32 / row_count as f32;
2078 f
2079 };
2080 let n_distinct = statistics::estimate_n_distinct(&non_null);
2081 let histogram_bounds = statistics::build_histogram(&non_null);
2082 self.statistics.set(
2083 table_name.to_string(),
2084 col_schema.name.clone(),
2085 statistics::ColumnStats {
2086 null_frac,
2087 n_distinct,
2088 histogram_bounds,
2089 },
2090 );
2091 }
2092 self.statistics.reset_modified(table_name);
2093 let cold_count = {
2099 let table = self
2100 .active_catalog()
2101 .get(table_name)
2102 .expect("table still present");
2103 table.count_cold_locators()
2104 };
2105 let table_mut = self
2106 .active_catalog_mut()
2107 .get_mut(table_name)
2108 .expect("table still present");
2109 table_mut.set_cold_row_count(cold_count);
2110 Ok(())
2111 }
2112
2113 fn exec_show_publications(&self) -> QueryResult {
2125 let columns = alloc::vec![
2126 ColumnSchema::new("name", DataType::Text, false),
2127 ColumnSchema::new("scope", DataType::Text, false),
2128 ColumnSchema::new("table_count", DataType::Int, true),
2129 ];
2130 let rows: Vec<Row> = self
2131 .publications
2132 .iter()
2133 .map(|(name, scope)| {
2134 let (scope_str, count_val) = match scope {
2135 spg_sql::ast::PublicationScope::AllTables => {
2136 ("FOR ALL TABLES".to_string(), Value::Null)
2137 }
2138 spg_sql::ast::PublicationScope::ForTables(ts) => (
2139 alloc::format!("FOR TABLE {}", ts.join(", ")),
2140 Value::Int(i32::try_from(ts.len()).unwrap_or(i32::MAX)),
2141 ),
2142 spg_sql::ast::PublicationScope::AllTablesExcept(ts) => (
2143 alloc::format!("FOR ALL TABLES EXCEPT {}", ts.join(", ")),
2144 Value::Int(i32::try_from(ts.len()).unwrap_or(i32::MAX)),
2145 ),
2146 };
2147 Row::new(alloc::vec![
2148 Value::Text(name.clone()),
2149 Value::Text(scope_str),
2150 count_val,
2151 ])
2152 })
2153 .collect();
2154 QueryResult::Rows { columns, rows }
2155 }
2156
2157 fn exec_show_users(&self) -> QueryResult {
2159 let columns = alloc::vec![
2160 ColumnSchema::new("name", DataType::Text, false),
2161 ColumnSchema::new("role", DataType::Text, false),
2162 ];
2163 let rows: Vec<Row> = self
2164 .users
2165 .iter()
2166 .map(|(name, rec)| {
2167 Row::new(alloc::vec![
2168 Value::Text(name.to_string()),
2169 Value::Text(rec.role.as_str().to_string()),
2170 ])
2171 })
2172 .collect();
2173 QueryResult::Rows { columns, rows }
2174 }
2175
2176 fn exec_create_user(&mut self, s: &CreateUserStatement) -> Result<QueryResult, EngineError> {
2177 if self.in_transaction() {
2178 return Err(EngineError::Unsupported(
2179 "CREATE USER is not allowed inside a transaction".into(),
2180 ));
2181 }
2182 let role = users::Role::parse(&s.role).ok_or_else(|| {
2183 EngineError::Unsupported(alloc::format!("invalid role: {:?}", s.role))
2184 })?;
2185 let salt = self.salt_fn.map_or_else(
2189 || {
2190 let mut s_bytes = [0u8; 16];
2191 let digest = spg_crypto::hash(s.name.as_bytes());
2192 s_bytes.copy_from_slice(&digest[..16]);
2193 s_bytes
2194 },
2195 |f| f(),
2196 );
2197 self.users
2198 .create(&s.name, &s.password, role, salt)
2199 .map_err(|e| EngineError::Unsupported(alloc::format!("CREATE USER: {e}")))?;
2200 Ok(QueryResult::CommandOk {
2201 affected: 1,
2202 modified_catalog: true,
2203 })
2204 }
2205
2206 fn exec_drop_user(&mut self, name: &str) -> Result<QueryResult, EngineError> {
2207 if self.in_transaction() {
2208 return Err(EngineError::Unsupported(
2209 "DROP USER is not allowed inside a transaction".into(),
2210 ));
2211 }
2212 self.users
2213 .drop(name)
2214 .map_err(|e| EngineError::Unsupported(alloc::format!("DROP USER: {e}")))?;
2215 Ok(QueryResult::CommandOk {
2216 affected: 1,
2217 modified_catalog: true,
2218 })
2219 }
2220
2221 fn exec_update_cancel(
2228 &mut self,
2229 stmt: &spg_sql::ast::UpdateStatement,
2230 cancel: CancelToken<'_>,
2231 ) -> Result<QueryResult, EngineError> {
2232 if let Some(w) = &stmt.where_ {
2240 let schema_cols = self
2241 .active_catalog()
2242 .get(&stmt.table)
2243 .ok_or_else(|| {
2244 EngineError::Storage(StorageError::TableNotFound {
2245 name: stmt.table.clone(),
2246 })
2247 })?
2248 .schema()
2249 .columns
2250 .clone();
2251 if let Some((col_pos, key)) = try_pk_predicate(w, &schema_cols, stmt.table.as_str())
2252 && let Some(idx_name) = self
2253 .active_catalog()
2254 .get(&stmt.table)
2255 .and_then(|t| t.index_on(col_pos).map(|i| i.name.clone()))
2256 {
2257 let _ = self
2261 .active_catalog_mut()
2262 .promote_cold_row(&stmt.table, &idx_name, &key);
2263 }
2264 }
2265
2266 let table = self
2267 .active_catalog_mut()
2268 .get_mut(&stmt.table)
2269 .ok_or_else(|| {
2270 EngineError::Storage(StorageError::TableNotFound {
2271 name: stmt.table.clone(),
2272 })
2273 })?;
2274 let schema_cols: Vec<ColumnSchema> = table.schema().columns.clone();
2275 let mut targets: Vec<(usize, &Expr)> = Vec::with_capacity(stmt.assignments.len());
2279 for (col, expr) in &stmt.assignments {
2280 let pos = schema_cols
2281 .iter()
2282 .position(|c| c.name == *col)
2283 .ok_or_else(|| {
2284 EngineError::Eval(EvalError::ColumnNotFound { name: col.clone() })
2285 })?;
2286 targets.push((pos, expr));
2287 }
2288 let ctx = EvalContext::new(&schema_cols, Some(stmt.table.as_str()));
2289 let mut planned: Vec<(usize, Vec<Value>)> = Vec::new();
2295 for (i, row) in table.rows().iter().enumerate() {
2296 if i.is_multiple_of(256) {
2300 cancel.check()?;
2301 }
2302 if let Some(w) = &stmt.where_ {
2303 let cond = eval::eval_expr(w, row, &ctx)?;
2304 if !matches!(cond, Value::Bool(true)) {
2305 continue;
2306 }
2307 }
2308 let mut new_vals = row.values.clone();
2309 for (pos, expr) in &targets {
2310 let v = eval::eval_expr(expr, row, &ctx)?;
2311 new_vals[*pos] =
2312 coerce_value(v, schema_cols[*pos].ty, &schema_cols[*pos].name, *pos)?;
2313 }
2314 planned.push((i, new_vals));
2315 }
2316 let plan_with_old: Vec<(usize, Vec<Value>, Vec<Value>)> = planned
2320 .iter()
2321 .map(|(pos, new_vals)| (*pos, table.rows()[*pos].values.clone(), new_vals.clone()))
2322 .collect();
2323 let self_fks = table.schema().foreign_keys.clone();
2324 let affected = planned.len();
2325 let _ = table;
2327 if !self_fks.is_empty() {
2331 let new_rows: Vec<Vec<Value>> = planned
2332 .iter()
2333 .map(|(_pos, new_vals)| new_vals.clone())
2334 .collect();
2335 enforce_fk_inserts(self.active_catalog(), &stmt.table, &self_fks, &new_rows)?;
2336 }
2337 let child_plan = plan_fk_parent_updates(self.active_catalog(), &stmt.table, &plan_with_old)?;
2341 for step in &child_plan {
2343 apply_fk_child_step(self.active_catalog_mut(), step)?;
2344 }
2345 let table = self
2347 .active_catalog_mut()
2348 .get_mut(&stmt.table)
2349 .ok_or_else(|| {
2350 EngineError::Storage(StorageError::TableNotFound {
2351 name: stmt.table.clone(),
2352 })
2353 })?;
2354 let updated_for_returning: Vec<Vec<Value>> =
2356 if stmt.returning.is_some() {
2357 planned.iter().map(|(_pos, vals)| vals.clone()).collect()
2358 } else {
2359 Vec::new()
2360 };
2361 for (pos, vals) in planned {
2362 table.update_row(pos, vals)?;
2363 }
2364 let _ = table;
2365 if !self.in_transaction() && affected > 0 {
2367 self.statistics
2368 .record_modifications(&stmt.table, affected as u64);
2369 }
2370 if let Some(items) = &stmt.returning {
2372 return self.build_returning_rows(
2373 &stmt.table,
2374 items,
2375 updated_for_returning,
2376 );
2377 }
2378 Ok(QueryResult::CommandOk {
2379 affected,
2380 modified_catalog: !self.in_transaction(),
2381 })
2382 }
2383
2384 fn exec_delete_cancel(
2388 &mut self,
2389 stmt: &spg_sql::ast::DeleteStatement,
2390 cancel: CancelToken<'_>,
2391 ) -> Result<QueryResult, EngineError> {
2392 let mut cold_shadow_count: usize = 0;
2400 if let Some(w) = &stmt.where_ {
2401 let schema_cols = self
2402 .active_catalog()
2403 .get(&stmt.table)
2404 .ok_or_else(|| {
2405 EngineError::Storage(StorageError::TableNotFound {
2406 name: stmt.table.clone(),
2407 })
2408 })?
2409 .schema()
2410 .columns
2411 .clone();
2412 if let Some((col_pos, key)) = try_pk_predicate(w, &schema_cols, stmt.table.as_str())
2413 && let Some(idx_name) = self
2414 .active_catalog()
2415 .get(&stmt.table)
2416 .and_then(|t| t.index_on(col_pos).map(|i| i.name.clone()))
2417 {
2418 cold_shadow_count = self
2419 .active_catalog_mut()
2420 .shadow_cold_row(&stmt.table, &idx_name, &key)
2421 .unwrap_or(0);
2422 }
2423 }
2424
2425 let table = self
2426 .active_catalog_mut()
2427 .get_mut(&stmt.table)
2428 .ok_or_else(|| {
2429 EngineError::Storage(StorageError::TableNotFound {
2430 name: stmt.table.clone(),
2431 })
2432 })?;
2433 let schema_cols: Vec<ColumnSchema> = table.schema().columns.clone();
2434 let ctx = EvalContext::new(&schema_cols, Some(stmt.table.as_str()));
2435 let mut positions: Vec<usize> = Vec::new();
2436 let mut to_delete_rows: Vec<Vec<Value>> = Vec::new();
2440 for (i, row) in table.rows().iter().enumerate() {
2441 if i.is_multiple_of(256) {
2442 cancel.check()?;
2443 }
2444 let keep = if let Some(w) = &stmt.where_ {
2445 let cond = eval::eval_expr(w, row, &ctx)?;
2446 !matches!(cond, Value::Bool(true))
2447 } else {
2448 false
2449 };
2450 if !keep {
2451 positions.push(i);
2452 to_delete_rows.push(row.values.clone());
2453 }
2454 }
2455 let _ = table;
2462 let cascade_plan = plan_fk_parent_deletions(
2463 self.active_catalog(),
2464 &stmt.table,
2465 &positions,
2466 &to_delete_rows,
2467 )?;
2468 for step in &cascade_plan {
2475 apply_fk_child_step(self.active_catalog_mut(), step)?;
2476 }
2477 let table = self
2479 .active_catalog_mut()
2480 .get_mut(&stmt.table)
2481 .ok_or_else(|| {
2482 EngineError::Storage(StorageError::TableNotFound {
2483 name: stmt.table.clone(),
2484 })
2485 })?;
2486 let affected = table.delete_rows(&positions) + cold_shadow_count;
2487 let _ = table;
2488 if !self.in_transaction() && affected > 0 {
2490 self.statistics
2491 .record_modifications(&stmt.table, affected as u64);
2492 }
2493 if let Some(items) = &stmt.returning {
2499 return self.build_returning_rows(
2500 &stmt.table,
2501 items,
2502 to_delete_rows,
2503 );
2504 }
2505 Ok(QueryResult::CommandOk {
2506 affected,
2507 modified_catalog: !self.in_transaction(),
2508 })
2509 }
2510
2511 #[allow(clippy::format_push_string)]
2521 fn exec_explain(
2522 &self,
2523 e: &spg_sql::ast::ExplainStatement,
2524 cancel: CancelToken<'_>,
2525 ) -> Result<QueryResult, EngineError> {
2526 let mut lines = Vec::<String>::new();
2527 explain_select(&e.inner, self, 0, &mut lines);
2528 if e.suggest {
2529 let suggestions = build_index_suggestions(&e.inner, self);
2538 for s in suggestions {
2539 lines.push(s);
2540 }
2541 } else if e.analyze {
2542 let started = self.clock.map(|f| f());
2559 let exec = self.exec_select_cancel(&e.inner, cancel)?;
2560 let elapsed_micros = match (self.clock, started) {
2561 (Some(f), Some(s)) => Some(f().saturating_sub(s)),
2562 _ => None,
2563 };
2564 let row_count = if let QueryResult::Rows { rows, .. } = &exec {
2565 rows.len()
2566 } else {
2567 0
2568 };
2569 annotate_explain_lines(&mut lines, row_count, self);
2570 let mut total = alloc::format!("Total: rows={row_count}");
2571 if let Some(us) = elapsed_micros {
2572 total.push_str(&alloc::format!(" elapsed={us}us"));
2573 }
2574 lines.push(total);
2575 }
2576 let columns = alloc::vec![ColumnSchema::new("QUERY PLAN", DataType::Text, false)];
2577 let rows: Vec<Row> = lines
2578 .into_iter()
2579 .map(|l| Row::new(alloc::vec![Value::Text(l)]))
2580 .collect();
2581 Ok(QueryResult::Rows { columns, rows })
2582 }
2583
2584 fn exec_show_tables(&self) -> QueryResult {
2585 let columns = alloc::vec![ColumnSchema::new("name", DataType::Text, false)];
2586 let rows: Vec<Row> = self
2587 .active_catalog()
2588 .table_names()
2589 .into_iter()
2590 .map(|n| Row::new(alloc::vec![Value::Text(n)]))
2591 .collect();
2592 QueryResult::Rows { columns, rows }
2593 }
2594
2595 fn exec_show_columns(&self, table_name: &str) -> Result<QueryResult, EngineError> {
2598 let table =
2599 self.active_catalog()
2600 .get(table_name)
2601 .ok_or_else(|| StorageError::TableNotFound {
2602 name: table_name.into(),
2603 })?;
2604 let columns = alloc::vec![
2605 ColumnSchema::new("name", DataType::Text, false),
2606 ColumnSchema::new("type", DataType::Text, false),
2607 ColumnSchema::new("nullable", DataType::Bool, false),
2608 ];
2609 let rows: Vec<Row> = table
2610 .schema()
2611 .columns
2612 .iter()
2613 .map(|c| {
2614 Row::new(alloc::vec![
2615 Value::Text(c.name.clone()),
2616 Value::Text(alloc::format!("{}", c.ty)),
2617 Value::Bool(c.nullable),
2618 ])
2619 })
2620 .collect();
2621 Ok(QueryResult::Rows { columns, rows })
2622 }
2623
2624 fn exec_begin(&mut self) -> Result<QueryResult, EngineError> {
2625 let tx_id = self.current_tx.ok_or(EngineError::NoActiveTransaction)?;
2626 if self.tx_catalogs.contains_key(&tx_id) {
2627 return Err(EngineError::TransactionAlreadyOpen);
2628 }
2629 self.tx_catalogs.insert(
2630 tx_id,
2631 TxState {
2632 catalog: self.catalog.clone(),
2633 savepoints: Vec::new(),
2634 },
2635 );
2636 Ok(QueryResult::CommandOk {
2637 affected: 0,
2638 modified_catalog: false,
2639 })
2640 }
2641
2642 fn exec_commit(&mut self) -> Result<QueryResult, EngineError> {
2643 let tx_id = self.current_tx.ok_or(EngineError::NoActiveTransaction)?;
2644 let state = self
2645 .tx_catalogs
2646 .remove(&tx_id)
2647 .ok_or(EngineError::NoActiveTransaction)?;
2648 self.catalog = state.catalog;
2649 Ok(QueryResult::CommandOk {
2653 affected: 0,
2654 modified_catalog: true,
2655 })
2656 }
2657
2658 fn exec_rollback(&mut self) -> Result<QueryResult, EngineError> {
2659 let tx_id = self.current_tx.ok_or(EngineError::NoActiveTransaction)?;
2660 if self.tx_catalogs.remove(&tx_id).is_none() {
2661 return Err(EngineError::NoActiveTransaction);
2662 }
2663 Ok(QueryResult::CommandOk {
2665 affected: 0,
2666 modified_catalog: false,
2667 })
2668 }
2669
2670 fn exec_savepoint(&mut self, name: String) -> Result<QueryResult, EngineError> {
2671 let tx_id = self.current_tx.ok_or(EngineError::NoActiveTransaction)?;
2672 let state = self
2673 .tx_catalogs
2674 .get_mut(&tx_id)
2675 .ok_or(EngineError::NoActiveTransaction)?;
2676 state.savepoints.retain(|(n, _)| n != &name);
2680 let snapshot = state.catalog.clone();
2681 state.savepoints.push((name, snapshot));
2682 Ok(QueryResult::CommandOk {
2683 affected: 0,
2684 modified_catalog: false,
2685 })
2686 }
2687
2688 fn exec_rollback_to_savepoint(&mut self, name: &str) -> Result<QueryResult, EngineError> {
2689 let tx_id = self.current_tx.ok_or(EngineError::NoActiveTransaction)?;
2690 let state = self
2691 .tx_catalogs
2692 .get_mut(&tx_id)
2693 .ok_or(EngineError::NoActiveTransaction)?;
2694 let pos = state
2695 .savepoints
2696 .iter()
2697 .rposition(|(n, _)| n == name)
2698 .ok_or_else(|| {
2699 EngineError::Unsupported(alloc::format!("savepoint not found: {name}"))
2700 })?;
2701 let snapshot = state.savepoints[pos].1.clone();
2705 state.savepoints.truncate(pos + 1);
2706 state.catalog = snapshot;
2707 Ok(QueryResult::CommandOk {
2708 affected: 0,
2709 modified_catalog: false,
2710 })
2711 }
2712
2713 fn exec_release_savepoint(&mut self, name: &str) -> Result<QueryResult, EngineError> {
2714 let tx_id = self.current_tx.ok_or(EngineError::NoActiveTransaction)?;
2715 let state = self
2716 .tx_catalogs
2717 .get_mut(&tx_id)
2718 .ok_or(EngineError::NoActiveTransaction)?;
2719 let pos = state
2720 .savepoints
2721 .iter()
2722 .rposition(|(n, _)| n == name)
2723 .ok_or_else(|| {
2724 EngineError::Unsupported(alloc::format!("savepoint not found: {name}"))
2725 })?;
2726 state.savepoints.truncate(pos);
2729 Ok(QueryResult::CommandOk {
2730 affected: 0,
2731 modified_catalog: false,
2732 })
2733 }
2734
2735 fn exec_alter_table(
2746 &mut self,
2747 s: spg_sql::ast::AlterTableStatement,
2748 ) -> Result<QueryResult, EngineError> {
2749 match s.target {
2750 spg_sql::ast::AlterTableTarget::SetHotTierBytes(n) => {
2751 let table = self
2752 .active_catalog_mut()
2753 .get_mut(&s.name)
2754 .ok_or_else(|| {
2755 EngineError::Storage(StorageError::TableNotFound {
2756 name: s.name.clone(),
2757 })
2758 })?;
2759 table.schema_mut().hot_tier_bytes = Some(n);
2760 }
2761 spg_sql::ast::AlterTableTarget::AddForeignKey(fk) => {
2762 let cols_snapshot = self
2767 .active_catalog()
2768 .get(&s.name)
2769 .ok_or_else(|| {
2770 EngineError::Storage(StorageError::TableNotFound {
2771 name: s.name.clone(),
2772 })
2773 })?
2774 .schema()
2775 .columns
2776 .clone();
2777 let storage_fk = resolve_foreign_key(
2778 &s.name,
2779 &cols_snapshot,
2780 fk,
2781 self.active_catalog(),
2782 )?;
2783 let existing_rows: Vec<Vec<Value>> = self
2786 .active_catalog()
2787 .get(&s.name)
2788 .expect("checked above")
2789 .rows()
2790 .iter()
2791 .map(|r| r.values.clone())
2792 .collect();
2793 enforce_fk_inserts(
2794 self.active_catalog(),
2795 &s.name,
2796 core::slice::from_ref(&storage_fk),
2797 &existing_rows,
2798 )?;
2799 let table = self
2801 .active_catalog_mut()
2802 .get_mut(&s.name)
2803 .expect("checked above");
2804 if let Some(name) = &storage_fk.name
2805 && table
2806 .schema()
2807 .foreign_keys
2808 .iter()
2809 .any(|f| f.name.as_ref() == Some(name))
2810 {
2811 return Err(EngineError::Unsupported(alloc::format!(
2812 "ALTER TABLE ADD CONSTRAINT: a constraint named {name:?} already exists"
2813 )));
2814 }
2815 table.schema_mut().foreign_keys.push(storage_fk);
2816 }
2817 spg_sql::ast::AlterTableTarget::DropForeignKey(name) => {
2818 let table = self
2819 .active_catalog_mut()
2820 .get_mut(&s.name)
2821 .ok_or_else(|| {
2822 EngineError::Storage(StorageError::TableNotFound {
2823 name: s.name.clone(),
2824 })
2825 })?;
2826 let fks = &mut table.schema_mut().foreign_keys;
2827 let before = fks.len();
2828 fks.retain(|f| f.name.as_ref() != Some(&name));
2829 if fks.len() == before {
2830 return Err(EngineError::Unsupported(alloc::format!(
2831 "ALTER TABLE DROP CONSTRAINT: no FK named {name:?} on {:?}",
2832 s.name
2833 )));
2834 }
2835 }
2836 }
2837 Ok(QueryResult::CommandOk {
2838 affected: 0,
2839 modified_catalog: !self.in_transaction(),
2840 })
2841 }
2842
2843 fn exec_alter_index(
2844 &mut self,
2845 stmt: spg_sql::ast::AlterIndexStatement,
2846 ) -> Result<QueryResult, EngineError> {
2847 let spg_sql::ast::AlterIndexStatement {
2851 name: idx_name,
2852 target,
2853 } = stmt;
2854 let spg_sql::ast::AlterIndexTarget::Rebuild { encoding } = target;
2855 let target = encoding.map(|e| match e {
2856 SqlVecEncoding::F32 => VecEncoding::F32,
2857 SqlVecEncoding::Sq8 => VecEncoding::Sq8,
2858 SqlVecEncoding::F16 => VecEncoding::F16,
2859 });
2860 let table_name = {
2865 let cat = self.active_catalog();
2866 let mut found: Option<String> = None;
2867 for tname in cat.table_names() {
2868 if let Some(t) = cat.get(&tname)
2869 && t.indices().iter().any(|i| i.name == idx_name)
2870 {
2871 found = Some(tname);
2872 break;
2873 }
2874 }
2875 found.ok_or_else(|| {
2876 EngineError::Storage(StorageError::IndexNotFound {
2877 name: idx_name.clone(),
2878 })
2879 })?
2880 };
2881 let table = self
2882 .active_catalog_mut()
2883 .get_mut(&table_name)
2884 .expect("table found above");
2885 table.rebuild_nsw_index(&idx_name, target)?;
2886 self.plan_cache.evict_referencing(&table_name);
2889 Ok(QueryResult::CommandOk {
2890 affected: 0,
2891 modified_catalog: !self.in_transaction(),
2892 })
2893 }
2894
2895 fn exec_create_index(
2896 &mut self,
2897 stmt: CreateIndexStatement,
2898 ) -> Result<QueryResult, EngineError> {
2899 let table = self
2900 .active_catalog_mut()
2901 .get_mut(&stmt.table)
2902 .ok_or_else(|| {
2903 EngineError::Storage(StorageError::TableNotFound {
2904 name: stmt.table.clone(),
2905 })
2906 })?;
2907 if stmt.if_not_exists && table.indices().iter().any(|i| i.name == stmt.name) {
2909 return Ok(QueryResult::CommandOk {
2910 affected: 0,
2911 modified_catalog: false,
2912 });
2913 }
2914 let _ = &stmt.extra_columns; let table_name = stmt.table.clone();
2921 let included_positions: Vec<usize> = if stmt.included_columns.is_empty() {
2925 Vec::new()
2926 } else {
2927 let schema = table.schema();
2928 stmt.included_columns
2929 .iter()
2930 .map(|c| {
2931 schema.column_position(c).ok_or_else(|| {
2932 EngineError::Storage(StorageError::ColumnNotFound {
2933 column: c.clone(),
2934 })
2935 })
2936 })
2937 .collect::<Result<Vec<_>, _>>()?
2938 };
2939 match stmt.method {
2940 IndexMethod::BTree => table.add_index(stmt.name.clone(), &stmt.column)?,
2941 IndexMethod::Hnsw => {
2942 if !included_positions.is_empty() {
2943 return Err(EngineError::Unsupported(
2944 "INCLUDE columns are not supported on HNSW indexes".into(),
2945 ));
2946 }
2947 table.add_nsw_index(stmt.name.clone(), &stmt.column, spg_storage::NSW_DEFAULT_M)?;
2948 }
2949 IndexMethod::Brin => {
2951 if !included_positions.is_empty() {
2952 return Err(EngineError::Unsupported(
2953 "INCLUDE columns are not supported on BRIN indexes".into(),
2954 ));
2955 }
2956 table.add_brin_index(stmt.name.clone(), &stmt.column)?;
2957 }
2958 }
2959 if !included_positions.is_empty()
2960 && let Some(idx) = table.indices_mut().iter_mut().find(|i| i.name == stmt.name)
2961 {
2962 idx.included_columns = included_positions;
2963 }
2964 if let Some(pred_expr) = &stmt.partial_predicate {
2972 let canonical = pred_expr.to_string();
2973 if matches!(stmt.method, IndexMethod::Hnsw | IndexMethod::Brin) {
2974 return Err(EngineError::Unsupported(
2975 "WHERE predicates are not supported on HNSW or BRIN indexes".into(),
2976 ));
2977 }
2978 if let Some(idx) = table.indices_mut().iter_mut().find(|i| i.name == stmt.name) {
2979 idx.partial_predicate = Some(canonical);
2980 }
2981 }
2982 if let Some(key_expr) = &stmt.expression {
2990 if matches!(stmt.method, IndexMethod::Hnsw | IndexMethod::Brin) {
2991 return Err(EngineError::Unsupported(
2992 "Expression keys are not supported on HNSW or BRIN indexes".into(),
2993 ));
2994 }
2995 let canonical = key_expr.to_string();
2996 if let Some(idx) = table.indices_mut().iter_mut().find(|i| i.name == stmt.name) {
2997 idx.expression = Some(canonical);
2998 }
2999 }
3000 self.plan_cache.evict_referencing(&table_name);
3003 Ok(QueryResult::CommandOk {
3004 affected: 0,
3005 modified_catalog: !self.in_transaction(),
3006 })
3007 }
3008
3009 fn exec_create_table(
3010 &mut self,
3011 stmt: CreateTableStatement,
3012 ) -> Result<QueryResult, EngineError> {
3013 if stmt.if_not_exists && self.active_catalog().get(&stmt.name).is_some() {
3014 return Ok(QueryResult::CommandOk {
3015 affected: 0,
3016 modified_catalog: false,
3017 });
3018 }
3019 let table_name = stmt.name.clone();
3020 let inline_pk_columns: Vec<String> = stmt
3024 .columns
3025 .iter()
3026 .filter(|c| c.is_primary_key)
3027 .map(|c| c.name.clone())
3028 .collect();
3029 let cols = stmt
3035 .columns
3036 .into_iter()
3037 .map(column_def_to_schema)
3038 .collect::<Result<Vec<_>, _>>()?;
3039 let mut cols = cols;
3041 for tc in &stmt.table_constraints {
3042 if let spg_sql::ast::TableConstraint::PrimaryKey { columns, .. } = tc {
3043 for col_name in columns {
3044 if let Some(col) = cols.iter_mut().find(|c| c.name == *col_name) {
3045 col.nullable = false;
3046 }
3047 }
3048 }
3049 }
3050 let mut fks: Vec<spg_storage::ForeignKeyConstraint> =
3057 Vec::with_capacity(stmt.foreign_keys.len());
3058 for fk in stmt.foreign_keys {
3059 fks.push(resolve_foreign_key(
3060 &table_name,
3061 &cols,
3062 fk,
3063 self.active_catalog(),
3064 )?);
3065 }
3066 let mut schema = TableSchema::new(table_name.clone(), cols);
3067 schema.foreign_keys = fks;
3068 let mut uc_storage: Vec<spg_storage::UniquenessConstraint> = Vec::new();
3072 for tc in &stmt.table_constraints {
3073 let (is_pk, names) = match tc {
3074 spg_sql::ast::TableConstraint::PrimaryKey { columns, .. } => {
3075 (true, columns.clone())
3076 }
3077 spg_sql::ast::TableConstraint::Unique { columns, .. } => {
3078 (false, columns.clone())
3079 }
3080 };
3081 let mut positions = Vec::with_capacity(names.len());
3082 for n in &names {
3083 let pos = schema
3084 .columns
3085 .iter()
3086 .position(|c| c.name == *n)
3087 .ok_or_else(|| {
3088 EngineError::Unsupported(alloc::format!(
3089 "table constraint references unknown column {n:?}"
3090 ))
3091 })?;
3092 positions.push(pos);
3093 }
3094 uc_storage.push(spg_storage::UniquenessConstraint {
3095 is_primary_key: is_pk,
3096 columns: positions,
3097 });
3098 }
3099 schema.uniqueness_constraints = uc_storage.clone();
3100 self.active_catalog_mut().create_table(schema)?;
3101 let table = self
3105 .active_catalog_mut()
3106 .get_mut(&table_name)
3107 .expect("just created");
3108 for (i, col_name) in inline_pk_columns.iter().enumerate() {
3109 let idx_name = if inline_pk_columns.len() == 1 {
3110 alloc::format!("{table_name}_pkey")
3111 } else {
3112 alloc::format!("{table_name}_pkey_{i}")
3113 };
3114 if let Err(e) = table.add_index(idx_name, col_name) {
3115 return Err(EngineError::Storage(e));
3116 }
3117 }
3118 for (i, tc) in stmt.table_constraints.iter().enumerate() {
3119 let (is_pk, names) = match tc {
3120 spg_sql::ast::TableConstraint::PrimaryKey { columns, .. } => {
3121 (true, columns)
3122 }
3123 spg_sql::ast::TableConstraint::Unique { columns, .. } => {
3124 (false, columns)
3125 }
3126 };
3127 let leading = &names[0];
3128 let already = table
3131 .indices()
3132 .iter()
3133 .any(|idx| {
3134 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
3135 && table.schema().columns[idx.column_position].name == *leading
3136 });
3137 if already {
3138 continue;
3139 }
3140 let suffix = if is_pk { "pkey" } else { "key" };
3141 let idx_name = if names.len() == 1 {
3142 alloc::format!("{table_name}_{leading}_{suffix}")
3143 } else {
3144 alloc::format!("{table_name}_{leading}_{suffix}_{i}")
3145 };
3146 if let Err(e) = table.add_index(idx_name, leading) {
3147 return Err(EngineError::Storage(e));
3148 }
3149 }
3150 Ok(QueryResult::CommandOk {
3151 affected: 0,
3152 modified_catalog: !self.in_transaction(),
3153 })
3154 }
3155
3156 fn exec_insert(&mut self, stmt: InsertStatement) -> Result<QueryResult, EngineError> {
3157 let clock = self.clock;
3161 let table = self
3162 .active_catalog_mut()
3163 .get_mut(&stmt.table)
3164 .ok_or_else(|| {
3165 EngineError::Storage(StorageError::TableNotFound {
3166 name: stmt.table.clone(),
3167 })
3168 })?;
3169 let column_meta: Vec<ColumnSchema> = table.schema().columns.clone();
3175 let schema_cols_len = column_meta.len();
3176 let tuple_pos: Option<Vec<Option<usize>>> = match &stmt.columns {
3180 None => None, Some(cols) => {
3182 let mut map = alloc::vec![None; schema_cols_len];
3183 for (j, name) in cols.iter().enumerate() {
3184 let idx = column_meta
3185 .iter()
3186 .position(|c| c.name == *name)
3187 .ok_or_else(|| {
3188 EngineError::Eval(EvalError::ColumnNotFound { name: name.clone() })
3189 })?;
3190 if map[idx].is_some() {
3191 return Err(EngineError::Storage(StorageError::ArityMismatch {
3192 expected: schema_cols_len,
3193 actual: cols.len(),
3194 }));
3195 }
3196 map[idx] = Some(j);
3197 }
3198 for (i, col) in column_meta.iter().enumerate() {
3202 if map[i].is_none()
3203 && !col.nullable
3204 && col.default.is_none()
3205 && col.runtime_default.is_none()
3206 && !col.auto_increment
3207 {
3208 return Err(EngineError::Storage(StorageError::NullInNotNull {
3209 column: col.name.clone(),
3210 }));
3211 }
3212 }
3213 Some(map)
3214 }
3215 };
3216 let expected_tuple_len = stmt.columns.as_ref().map_or(schema_cols_len, Vec::len);
3217 let fks = table.schema().foreign_keys.clone();
3223 let mut affected = 0usize;
3224 let mut all_values: Vec<Vec<Value>> = Vec::with_capacity(stmt.rows.len());
3227 for tuple in stmt.rows {
3228 if tuple.len() != expected_tuple_len {
3229 return Err(EngineError::Storage(StorageError::ArityMismatch {
3230 expected: expected_tuple_len,
3231 actual: tuple.len(),
3232 }));
3233 }
3234 let values: Vec<Value> = if let Some(map) = &tuple_pos {
3238 let raw_tuple: Vec<Value> = tuple
3240 .into_iter()
3241 .map(literal_expr_to_value)
3242 .collect::<Result<_, _>>()?;
3243 let mut out = Vec::with_capacity(schema_cols_len);
3244 for (i, col) in column_meta.iter().enumerate() {
3245 let mut raw = match map[i] {
3246 Some(j) => raw_tuple[j].clone(),
3247 None => resolve_column_default_free(col, clock)?,
3248 };
3249 if col.auto_increment && raw.is_null() {
3250 let next = table.next_auto_value(i).ok_or_else(|| {
3251 EngineError::Unsupported(alloc::format!(
3252 "AUTO_INCREMENT applies to integer columns only (column `{}`)",
3253 col.name
3254 ))
3255 })?;
3256 raw = Value::BigInt(next);
3257 }
3258 out.push(coerce_value(raw, col.ty, &col.name, i)?);
3259 }
3260 out
3261 } else {
3262 let mut out = Vec::with_capacity(schema_cols_len);
3264 for (i, (col, expr)) in column_meta.iter().zip(tuple).enumerate() {
3265 let mut raw = literal_expr_to_value(expr)?;
3266 if col.auto_increment && raw.is_null() {
3267 let next = table.next_auto_value(i).ok_or_else(|| {
3268 EngineError::Unsupported(alloc::format!(
3269 "AUTO_INCREMENT applies to integer columns only (column `{}`)",
3270 col.name
3271 ))
3272 })?;
3273 raw = Value::BigInt(next);
3274 }
3275 out.push(coerce_value(raw, col.ty, &col.name, i)?);
3276 }
3277 out
3278 };
3279 all_values.push(values);
3280 }
3281 let uniqueness = table.schema().uniqueness_constraints.clone();
3286 let _ = table;
3287 if !fks.is_empty() {
3288 enforce_fk_inserts(self.active_catalog(), &stmt.table, &fks, &all_values)?;
3289 }
3290 enforce_uniqueness_inserts(
3292 self.active_catalog(),
3293 &stmt.table,
3294 &uniqueness,
3295 &all_values,
3296 )?;
3297 let mut pending_updates: Vec<(usize, Vec<Value>)> = Vec::new();
3304 let mut skipped_count = 0usize;
3305 if let Some(clause) = &stmt.on_conflict {
3306 let conflict_cols = resolve_on_conflict_columns(
3307 self.active_catalog(),
3308 &stmt.table,
3309 clause.target_columns.as_slice(),
3310 )?;
3311 let mut kept: Vec<Vec<Value>> = Vec::with_capacity(all_values.len());
3312 let mut seen_keys: Vec<Vec<Value>> = Vec::new();
3313 for values in all_values {
3314 let key_tuple: Vec<&Value> =
3315 conflict_cols.iter().map(|&c| &values[c]).collect();
3316 let has_null_key = key_tuple.iter().any(|v| matches!(v, Value::Null));
3319 let collides_with_table = !has_null_key
3320 && on_conflict_keys_exist(
3321 self.active_catalog(),
3322 &stmt.table,
3323 &conflict_cols,
3324 &key_tuple,
3325 );
3326 let key_tuple_owned: Vec<Value> =
3327 key_tuple.iter().map(|v| (*v).clone()).collect();
3328 let collides_with_batch = !has_null_key
3329 && seen_keys.iter().any(|k| k == &key_tuple_owned);
3330 let collides = collides_with_table || collides_with_batch;
3331 match (&clause.action, collides) {
3332 (_, false) => {
3333 seen_keys.push(key_tuple_owned);
3334 kept.push(values);
3335 }
3336 (spg_sql::ast::OnConflictAction::Nothing, true) => {
3337 skipped_count += 1;
3338 }
3339 (
3340 spg_sql::ast::OnConflictAction::Update {
3341 assignments,
3342 where_,
3343 },
3344 true,
3345 ) => {
3346 if !collides_with_table {
3347 skipped_count += 1;
3348 continue;
3349 }
3350 let target_pos = lookup_row_position_by_keys(
3351 self.active_catalog(),
3352 &stmt.table,
3353 &conflict_cols,
3354 &key_tuple,
3355 )
3356 .ok_or_else(|| {
3357 EngineError::Unsupported(
3358 "ON CONFLICT DO UPDATE: conflict detected but row \
3359 position could not be resolved (cold-tier row?)"
3360 .into(),
3361 )
3362 })?;
3363 let updated = apply_on_conflict_assignments(
3364 self.active_catalog(),
3365 &stmt.table,
3366 target_pos,
3367 &values,
3368 assignments,
3369 where_.as_ref(),
3370 )?;
3371 if let Some(new_row) = updated {
3372 pending_updates.push((target_pos, new_row));
3373 } else {
3374 skipped_count += 1;
3375 }
3376 }
3377 }
3378 }
3379 all_values = kept;
3380 }
3381 let table = self
3383 .active_catalog_mut()
3384 .get_mut(&stmt.table)
3385 .ok_or_else(|| {
3386 EngineError::Storage(StorageError::TableNotFound {
3387 name: stmt.table.clone(),
3388 })
3389 })?;
3390 let mut returning_rows: Vec<Vec<Value>> = Vec::new();
3394 for values in all_values {
3395 if stmt.returning.is_some() {
3396 returning_rows.push(values.clone());
3397 }
3398 table.insert(Row::new(values))?;
3399 affected += 1;
3400 }
3401 for (pos, new_row) in pending_updates {
3405 if stmt.returning.is_some() {
3406 returning_rows.push(new_row.clone());
3407 }
3408 table.update_row(pos, new_row)?;
3409 affected += 1;
3410 }
3411 let _ = skipped_count;
3412 if let Some(items) = &stmt.returning {
3416 let _ = table;
3417 return self.build_returning_rows(
3418 &stmt.table,
3419 items,
3420 returning_rows,
3421 );
3422 }
3423 if !self.in_transaction() && affected > 0 {
3428 self.statistics
3429 .record_modifications(&stmt.table, affected as u64);
3430 }
3431 Ok(QueryResult::CommandOk {
3432 affected,
3433 modified_catalog: !self.in_transaction(),
3434 })
3435 }
3436
3437 fn exec_select_as_of_segment(
3450 &self,
3451 stmt: &SelectStatement,
3452 from: &spg_sql::ast::FromClause,
3453 segment_id: u32,
3454 ) -> Result<QueryResult, EngineError> {
3455 if !from.joins.is_empty()
3458 || stmt.group_by.is_some()
3459 || stmt.having.is_some()
3460 || !stmt.unions.is_empty()
3461 || !stmt.order_by.is_empty()
3462 || stmt.offset.is_some()
3463 || stmt.distinct
3464 || aggregate::uses_aggregate(stmt)
3465 {
3466 return Err(EngineError::Unsupported(
3467 "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
3468 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
3469 .into(),
3470 ));
3471 }
3472 let table = self
3473 .active_catalog()
3474 .get(&from.primary.name)
3475 .ok_or_else(|| StorageError::TableNotFound {
3476 name: from.primary.name.clone(),
3477 })?;
3478 let schema = table.schema().clone();
3479 let schema_cols = &schema.columns;
3480 let alias = from
3481 .primary
3482 .alias
3483 .as_deref()
3484 .unwrap_or(from.primary.name.as_str());
3485 let ctx = EvalContext::new(schema_cols, Some(alias));
3486 let seg = self
3487 .active_catalog()
3488 .cold_segment(segment_id)
3489 .ok_or_else(|| {
3490 EngineError::Unsupported(alloc::format!(
3491 "AS OF SEGMENT: cold segment {segment_id} not registered"
3492 ))
3493 })?;
3494 let mut out_rows: Vec<Row> = Vec::new();
3495 let mut limit_remaining: Option<usize> =
3496 stmt.limit.as_ref().and_then(|n| usize::try_from(*n).ok());
3497 for (_key, body) in seg.scan() {
3498 let (row, _consumed) = spg_storage::decode_row_body_dense(&body, &schema)
3499 .map_err(EngineError::Storage)?;
3500 if let Some(where_expr) = &stmt.where_ {
3501 let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
3502 if !matches!(cond, Value::Bool(true)) {
3503 continue;
3504 }
3505 }
3506 let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
3508 out_rows.push(projected);
3509 if let Some(rem) = limit_remaining.as_mut() {
3510 if *rem == 0 {
3511 out_rows.pop();
3512 break;
3513 }
3514 *rem -= 1;
3515 }
3516 }
3517 let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
3519 Ok(QueryResult::Rows {
3520 columns,
3521 rows: out_rows,
3522 })
3523 }
3524
3525 fn eval_expr_simple(
3530 &self,
3531 expr: &Expr,
3532 row: &Row,
3533 ctx: &EvalContext,
3534 ) -> Result<Value, EngineError> {
3535 let cancel = CancelToken::none();
3536 self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
3537 }
3538
3539 fn build_returning_rows(
3546 &self,
3547 table_name: &str,
3548 items: &[SelectItem],
3549 mutated_rows: Vec<Vec<Value>>,
3550 ) -> Result<QueryResult, EngineError> {
3551 let table = self.active_catalog().get(table_name).ok_or_else(|| {
3552 EngineError::Storage(StorageError::TableNotFound {
3553 name: table_name.into(),
3554 })
3555 })?;
3556 let schema_cols = table.schema().columns.clone();
3557 let columns = self.derive_output_columns(items, &schema_cols, table_name);
3558 let mut out_rows: Vec<Row> = Vec::with_capacity(mutated_rows.len());
3559 for values in mutated_rows {
3560 let row = Row::new(values);
3561 let projected = self.project_row_simple(&row, items, &schema_cols, table_name)?;
3562 out_rows.push(projected);
3563 }
3564 Ok(QueryResult::Rows {
3565 columns,
3566 rows: out_rows,
3567 })
3568 }
3569
3570 fn project_row_simple(
3574 &self,
3575 row: &Row,
3576 items: &[SelectItem],
3577 schema_cols: &[ColumnSchema],
3578 alias: &str,
3579 ) -> Result<Row, EngineError> {
3580 let ctx = EvalContext::new(schema_cols, Some(alias));
3581 let cancel = CancelToken::none();
3582 let mut out_vals = Vec::new();
3583 for item in items {
3584 match item {
3585 SelectItem::Wildcard => {
3586 out_vals.extend(row.values.iter().cloned());
3587 }
3588 SelectItem::Expr { expr, .. } => {
3589 let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
3590 out_vals.push(v);
3591 }
3592 }
3593 }
3594 Ok(Row::new(out_vals))
3595 }
3596
3597 fn derive_output_columns(
3602 &self,
3603 items: &[SelectItem],
3604 schema_cols: &[ColumnSchema],
3605 _alias: &str,
3606 ) -> Vec<ColumnSchema> {
3607 let mut out = Vec::new();
3608 for item in items {
3609 match item {
3610 SelectItem::Wildcard => {
3611 out.extend(schema_cols.iter().cloned());
3612 }
3613 SelectItem::Expr { alias, .. } => {
3614 let name = alias
3615 .clone()
3616 .unwrap_or_else(|| "?column?".to_string());
3617 out.push(ColumnSchema::new(name, DataType::Text, true));
3620 }
3621 }
3622 }
3623 out
3624 }
3625
3626 fn exec_select_cancel(
3627 &self,
3628 stmt: &SelectStatement,
3629 cancel: CancelToken<'_>,
3630 ) -> Result<QueryResult, EngineError> {
3631 cancel.check()?;
3632 if let Some(from) = &stmt.from
3641 && let Some(seg_id) = from.primary.as_of_segment
3642 {
3643 return self.exec_select_as_of_segment(stmt, from, seg_id);
3644 }
3645 if let Some(from) = &stmt.from
3649 && from.joins.is_empty()
3650 && stmt.where_.is_none()
3651 && stmt.group_by.is_none()
3652 && stmt.having.is_none()
3653 && stmt.unions.is_empty()
3654 && stmt.order_by.is_empty()
3655 && stmt.limit.is_none()
3656 && stmt.offset.is_none()
3657 && !stmt.distinct
3658 && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard))
3659 {
3660 let lower = from.primary.name.to_ascii_lowercase();
3661 match lower.as_str() {
3662 "spg_statistic" => return Ok(self.exec_spg_statistic()),
3663 "spg_stat_replication" => return Ok(self.exec_spg_stat_replication()),
3665 "spg_stat_segment" => return Ok(self.exec_spg_stat_segment()),
3666 "spg_stat_query" => return Ok(self.exec_spg_stat_query()),
3667 "spg_stat_activity" => return Ok(self.exec_spg_stat_activity()),
3668 "spg_audit_chain" => return Ok(self.exec_spg_audit_chain()),
3669 "spg_audit_verify" => return Ok(self.exec_spg_audit_verify()),
3670 "spg_table_ddl" => return Ok(self.exec_spg_table_ddl()),
3671 "spg_role_ddl" => return Ok(self.exec_spg_role_ddl()),
3672 "spg_database_ddl" => return Ok(self.exec_spg_database_ddl()),
3673 _ => {}
3674 }
3675 }
3676 if !stmt.ctes.is_empty() {
3684 return self.exec_with_ctes(stmt, cancel);
3685 }
3686 let mut stmt_owned;
3693 let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3694 stmt_owned = stmt.clone();
3695 self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3696 &stmt_owned
3697 } else {
3698 stmt
3699 };
3700 if stmt_ref.unions.is_empty() {
3701 return self.exec_bare_select_cancel(stmt_ref, cancel);
3702 }
3703 let mut head = stmt_ref.clone();
3708 head.unions = Vec::new();
3709 head.order_by = Vec::new();
3710 head.limit = None;
3711 let QueryResult::Rows { columns, mut rows } =
3712 self.exec_bare_select_cancel(&head, cancel)?
3713 else {
3714 unreachable!("bare SELECT cannot return CommandOk")
3715 };
3716 for (kind, peer) in &stmt_ref.unions {
3717 let QueryResult::Rows {
3718 columns: peer_cols,
3719 rows: peer_rows,
3720 } = self.exec_bare_select_cancel(peer, cancel)?
3721 else {
3722 unreachable!("bare SELECT cannot return CommandOk")
3723 };
3724 if peer_cols.len() != columns.len() {
3725 return Err(EngineError::Unsupported(alloc::format!(
3726 "UNION arity mismatch: head has {} columns, peer has {}",
3727 columns.len(),
3728 peer_cols.len()
3729 )));
3730 }
3731 rows.extend(peer_rows);
3732 if matches!(kind, UnionKind::Distinct) {
3733 rows = dedup_rows(rows);
3734 }
3735 }
3736 if !stmt.order_by.is_empty() {
3739 let synth_ctx = EvalContext::new(&columns, None);
3740 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
3741 let mut tagged: Vec<(Vec<f64>, Row)> = Vec::with_capacity(rows.len());
3742 for r in rows {
3743 let keys = build_order_keys(&stmt.order_by, &r, &synth_ctx)?;
3744 tagged.push((keys, r));
3745 }
3746 sort_by_keys(&mut tagged, &descs);
3747 rows = tagged.into_iter().map(|(_, r)| r).collect();
3748 }
3749 apply_offset_and_limit(&mut rows, stmt.offset, stmt.limit);
3750 Ok(QueryResult::Rows { columns, rows })
3751 }
3752
3753 #[allow(clippy::too_many_lines)]
3754 #[allow(clippy::too_many_lines)] fn exec_bare_select_cancel(
3756 &self,
3757 stmt: &SelectStatement,
3758 cancel: CancelToken<'_>,
3759 ) -> Result<QueryResult, EngineError> {
3760 if select_has_window(stmt) {
3765 return self.exec_select_with_window(stmt, cancel);
3766 }
3767 let Some(from) = &stmt.from else {
3772 let empty_schema: Vec<ColumnSchema> = Vec::new();
3773 let ctx = EvalContext::new(&empty_schema, None);
3774 let projection = build_projection(&stmt.items, &empty_schema, "")?;
3775 let dummy_row = Row::new(Vec::new());
3776 let mut values = Vec::with_capacity(projection.len());
3777 for p in &projection {
3778 values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
3779 }
3780 let columns: Vec<ColumnSchema> = projection
3781 .into_iter()
3782 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
3783 .collect();
3784 return Ok(QueryResult::Rows {
3785 columns,
3786 rows: alloc::vec![Row::new(values)],
3787 });
3788 };
3789 if !from.joins.is_empty() {
3793 return self.exec_joined_select(stmt, from);
3794 }
3795 let primary = &from.primary;
3796 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
3797 StorageError::TableNotFound {
3798 name: primary.name.clone(),
3799 }
3800 })?;
3801 let schema_cols = &table.schema().columns;
3802 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
3805 let ctx = EvalContext::new(schema_cols, Some(alias));
3806
3807 if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias) {
3812 return materialise_in_order(stmt, table, schema_cols, alias, &nsw_rows);
3813 }
3814
3815 let indexed_rows: Option<Vec<Cow<'_, Row>>> = stmt
3823 .where_
3824 .as_ref()
3825 .and_then(|w| try_index_seek(w, schema_cols, self.active_catalog(), table, alias));
3826
3827 if aggregate::uses_aggregate(stmt) {
3830 let mut filtered: Vec<&Row> = Vec::new();
3831 let mut memo = memoize::MemoizeCache::new();
3835 if let Some(rows) = &indexed_rows {
3836 for cow in rows {
3837 let row = cow.as_ref();
3838 if let Some(where_expr) = &stmt.where_ {
3839 let cond = self.eval_expr_with_correlated(
3840 where_expr,
3841 row,
3842 &ctx,
3843 cancel,
3844 Some(&mut memo),
3845 )?;
3846 if !matches!(cond, Value::Bool(true)) {
3847 continue;
3848 }
3849 }
3850 filtered.push(row);
3851 }
3852 } else {
3853 for i in 0..table.row_count() {
3854 let row = &table.rows()[i];
3855 if let Some(where_expr) = &stmt.where_ {
3856 let cond = self.eval_expr_with_correlated(
3857 where_expr,
3858 row,
3859 &ctx,
3860 cancel,
3861 Some(&mut memo),
3862 )?;
3863 if !matches!(cond, Value::Bool(true)) {
3864 continue;
3865 }
3866 }
3867 filtered.push(row);
3868 }
3869 }
3870 let mut agg = aggregate::run(stmt, &filtered, schema_cols, Some(alias))?;
3871 apply_offset_and_limit(&mut agg.rows, stmt.offset, stmt.limit);
3872 return Ok(QueryResult::Rows {
3873 columns: agg.columns,
3874 rows: agg.rows,
3875 });
3876 }
3877
3878 let projection = build_projection(&stmt.items, schema_cols, alias)?;
3879
3880 let mut tagged: Vec<(Vec<f64>, Row)> = Vec::new();
3883 let mut memo = memoize::MemoizeCache::new();
3885 let mut process_row = |row: &Row, loop_idx: usize| -> Result<(), EngineError> {
3888 if loop_idx.is_multiple_of(256) {
3889 cancel.check()?;
3890 }
3891 if let Some(where_expr) = &stmt.where_ {
3892 let cond = self.eval_expr_with_correlated(
3893 where_expr,
3894 row,
3895 &ctx,
3896 cancel,
3897 Some(&mut memo),
3898 )?;
3899 if !matches!(cond, Value::Bool(true)) {
3900 return Ok(());
3901 }
3902 }
3903 let mut values = Vec::with_capacity(projection.len());
3904 for p in &projection {
3905 values.push(eval::eval_expr(&p.expr, row, &ctx)?);
3906 }
3907 let order_keys = if stmt.order_by.is_empty() {
3908 Vec::new()
3909 } else {
3910 build_order_keys(&stmt.order_by, row, &ctx)?
3911 };
3912 tagged.push((order_keys, Row::new(values)));
3913 Ok(())
3914 };
3915 if let Some(rows) = &indexed_rows {
3916 for (loop_idx, cow) in rows.iter().enumerate() {
3917 process_row(cow.as_ref(), loop_idx)?;
3918 }
3919 } else {
3920 for i in 0..table.row_count() {
3921 process_row(&table.rows()[i], i)?;
3922 }
3923 }
3924
3925 if !stmt.order_by.is_empty() {
3926 let keep = if stmt.distinct {
3931 None
3932 } else {
3933 stmt.limit
3934 .map(|l| l as usize + stmt.offset.map_or(0, |o| o as usize))
3935 };
3936 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
3937 partial_sort_tagged(&mut tagged, keep, &descs);
3938 }
3939
3940 let mut output_rows: Vec<Row> = tagged.into_iter().map(|(_, r)| r).collect();
3941 if stmt.distinct {
3942 output_rows = dedup_rows(output_rows);
3943 }
3944 apply_offset_and_limit(&mut output_rows, stmt.offset, stmt.limit);
3945
3946 let columns: Vec<ColumnSchema> = projection
3947 .into_iter()
3948 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
3949 .collect();
3950
3951 Ok(QueryResult::Rows {
3952 columns,
3953 rows: output_rows,
3954 })
3955 }
3956
3957 #[allow(clippy::too_many_lines)]
3964 fn exec_joined_select(
3965 &self,
3966 stmt: &SelectStatement,
3967 from: &FromClause,
3968 ) -> Result<QueryResult, EngineError> {
3969 let primary_table = self
3972 .active_catalog()
3973 .get(&from.primary.name)
3974 .ok_or_else(|| StorageError::TableNotFound {
3975 name: from.primary.name.clone(),
3976 })?;
3977 let primary_alias = from
3978 .primary
3979 .alias
3980 .as_deref()
3981 .unwrap_or(from.primary.name.as_str())
3982 .to_string();
3983 let mut joined_tables: Vec<(&Table, String, JoinKind, Option<&Expr>)> = Vec::new();
3984 for j in &from.joins {
3985 let t = self.active_catalog().get(&j.table.name).ok_or_else(|| {
3986 StorageError::TableNotFound {
3987 name: j.table.name.clone(),
3988 }
3989 })?;
3990 let a = j
3991 .table
3992 .alias
3993 .as_deref()
3994 .unwrap_or(j.table.name.as_str())
3995 .to_string();
3996 joined_tables.push((t, a, j.kind, j.on.as_ref()));
3997 }
3998
3999 let mut combined_schema: Vec<ColumnSchema> = Vec::new();
4002 for col in &primary_table.schema().columns {
4003 combined_schema.push(ColumnSchema::new(
4004 alloc::format!("{primary_alias}.{}", col.name),
4005 col.ty,
4006 col.nullable,
4007 ));
4008 }
4009 for (t, a, _, _) in &joined_tables {
4010 for col in &t.schema().columns {
4011 combined_schema.push(ColumnSchema::new(
4012 alloc::format!("{a}.{}", col.name),
4013 col.ty,
4014 col.nullable,
4015 ));
4016 }
4017 }
4018 let ctx = EvalContext::new(&combined_schema, None);
4019
4020 let mut working: Vec<Row> = primary_table.rows().iter().cloned().collect();
4023 let mut produced_len = primary_table.schema().columns.len();
4024 for (t, _, kind, on) in &joined_tables {
4025 let right_arity = t.schema().columns.len();
4026 let mut next: Vec<Row> = Vec::new();
4027 for left in &working {
4028 let mut left_matched = false;
4029 for right in t.rows() {
4030 let mut combined_vals = left.values.clone();
4031 combined_vals.extend(right.values.iter().cloned());
4032 let combined = Row::new(combined_vals);
4035 let keep = if let Some(on_expr) = on {
4036 let cond = eval::eval_expr(on_expr, &combined, &ctx)?;
4037 matches!(cond, Value::Bool(true))
4038 } else {
4039 true
4041 };
4042 if keep {
4043 next.push(combined);
4044 left_matched = true;
4045 }
4046 }
4047 if !left_matched && matches!(kind, JoinKind::Left) {
4048 let mut combined_vals = left.values.clone();
4051 for _ in 0..right_arity {
4052 combined_vals.push(Value::Null);
4053 }
4054 next.push(Row::new(combined_vals));
4055 }
4056 }
4057 working = next;
4058 produced_len += right_arity;
4059 debug_assert!(produced_len <= combined_schema.len());
4060 }
4061
4062 let mut filtered: Vec<Row> = Vec::new();
4064 for row in working {
4065 if let Some(where_expr) = &stmt.where_ {
4066 let cond = eval::eval_expr(where_expr, &row, &ctx)?;
4067 if !matches!(cond, Value::Bool(true)) {
4068 continue;
4069 }
4070 }
4071 filtered.push(row);
4072 }
4073
4074 if aggregate::uses_aggregate(stmt) {
4077 let refs: Vec<&Row> = filtered.iter().collect();
4078 let mut agg = aggregate::run(stmt, &refs, &combined_schema, None)?;
4079 apply_offset_and_limit(&mut agg.rows, stmt.offset, stmt.limit);
4080 return Ok(QueryResult::Rows {
4081 columns: agg.columns,
4082 rows: agg.rows,
4083 });
4084 }
4085
4086 let projection = build_projection(&stmt.items, &combined_schema, "")?;
4087 let mut tagged: Vec<(Vec<f64>, Row)> = Vec::new();
4088 for row in &filtered {
4089 let mut values = Vec::with_capacity(projection.len());
4090 for p in &projection {
4091 values.push(eval::eval_expr(&p.expr, row, &ctx)?);
4092 }
4093 let order_keys = if stmt.order_by.is_empty() {
4094 Vec::new()
4095 } else {
4096 build_order_keys(&stmt.order_by, row, &ctx)?
4097 };
4098 tagged.push((order_keys, Row::new(values)));
4099 }
4100 if !stmt.order_by.is_empty() {
4101 let keep = if stmt.distinct {
4102 None
4103 } else {
4104 stmt.limit
4105 .map(|l| l as usize + stmt.offset.map_or(0, |o| o as usize))
4106 };
4107 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
4108 partial_sort_tagged(&mut tagged, keep, &descs);
4109 }
4110 let mut output_rows: Vec<Row> = tagged.into_iter().map(|(_, r)| r).collect();
4111 if stmt.distinct {
4112 output_rows = dedup_rows(output_rows);
4113 }
4114 apply_offset_and_limit(&mut output_rows, stmt.offset, stmt.limit);
4115 let columns: Vec<ColumnSchema> = projection
4116 .into_iter()
4117 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
4118 .collect();
4119 Ok(QueryResult::Rows {
4120 columns,
4121 rows: output_rows,
4122 })
4123 }
4124}
4125
4126#[derive(Debug, Clone)]
4129struct ProjectedItem {
4130 expr: Expr,
4131 output_name: String,
4132 ty: DataType,
4133 nullable: bool,
4134}
4135
4136fn dedup_rows(rows: Vec<Row>) -> Vec<Row> {
4142 let mut out: Vec<Row> = Vec::with_capacity(rows.len());
4143 for r in rows {
4144 if !out.iter().any(|seen| seen == &r) {
4145 out.push(r);
4146 }
4147 }
4148 out
4149}
4150
4151fn value_to_order_key(v: &Value) -> Result<f64, EngineError> {
4155 match v {
4156 Value::Null => Ok(f64::INFINITY),
4157 Value::SmallInt(n) => Ok(f64::from(*n)),
4158 Value::Int(n) => Ok(f64::from(*n)),
4159 Value::Date(d) => Ok(f64::from(*d)),
4160 #[allow(clippy::cast_precision_loss)]
4161 Value::Timestamp(t) => Ok(*t as f64),
4162 #[allow(clippy::cast_precision_loss)]
4163 Value::Numeric { scaled, scale } => {
4164 let mut divisor = 1.0_f64;
4170 for _ in 0..*scale {
4171 divisor *= 10.0;
4172 }
4173 Ok((*scaled as f64) / divisor)
4174 }
4175 #[allow(clippy::cast_precision_loss)]
4176 Value::BigInt(n) => Ok(*n as f64),
4177 Value::Float(x) => Ok(*x),
4178 Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
4179 Value::Text(s) => {
4180 let mut key: u64 = 0;
4184 for &b in s.as_bytes().iter().take(8) {
4185 key = (key << 8) | u64::from(b);
4186 }
4187 #[allow(clippy::cast_precision_loss)]
4188 Ok(key as f64)
4189 }
4190 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
4191 Err(EngineError::Unsupported(
4192 "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
4193 ))
4194 }
4195 Value::Interval { .. } => Err(EngineError::Unsupported(
4196 "ORDER BY of an INTERVAL is not supported in v2.11 \
4197 (months vs micros has no single canonical ordering)"
4198 .into(),
4199 )),
4200 Value::Json(_) => Err(EngineError::Unsupported(
4201 "ORDER BY of a JSON value is not supported — cast the document to text first".into(),
4202 )),
4203 _ => Err(EngineError::Unsupported(
4207 "ORDER BY of this value type is not supported".into(),
4208 )),
4209 }
4210}
4211
4212fn try_nsw_knn(
4226 stmt: &SelectStatement,
4227 table: &Table,
4228 schema_cols: &[ColumnSchema],
4229 table_alias: &str,
4230) -> Option<Vec<usize>> {
4231 if stmt.distinct {
4232 return None;
4233 }
4234 let limit = usize::try_from(stmt.limit?).ok()?;
4235 if limit == 0 {
4236 return None;
4237 }
4238 if stmt.order_by.len() != 1 {
4242 return None;
4243 }
4244 let order = &stmt.order_by[0];
4245 if order.desc {
4249 return None;
4250 }
4251 let Expr::Binary { lhs, op, rhs } = &order.expr else {
4252 return None;
4253 };
4254 let metric = match op {
4255 BinOp::L2Distance => spg_storage::NswMetric::L2,
4256 BinOp::InnerProduct => spg_storage::NswMetric::InnerProduct,
4257 BinOp::CosineDistance => spg_storage::NswMetric::Cosine,
4258 _ => return None,
4259 };
4260 let ((Expr::Column(col), literal) | (literal, Expr::Column(col))) =
4262 (lhs.as_ref(), rhs.as_ref())
4263 else {
4264 return None;
4265 };
4266 if let Some(q) = &col.qualifier
4267 && q != table_alias
4268 {
4269 return None;
4270 }
4271 let col_pos = schema_cols.iter().position(|s| s.name == col.name)?;
4272 let query = literal_to_vector(literal)?;
4273 let idx = spg_storage::nsw_index_on(table, col_pos)?;
4274 if let Some(where_expr) = &stmt.where_ {
4275 let over_fetch = limit.saturating_mul(10).max(NSW_OVER_FETCH_FLOOR);
4279 let candidates = spg_storage::nsw_query(table, &idx.name, &query, over_fetch, metric);
4280 let ctx = EvalContext::new(schema_cols, Some(table_alias));
4281 let mut kept: Vec<usize> = Vec::with_capacity(limit);
4282 for i in candidates {
4283 let row = &table.rows()[i];
4284 let cond = eval::eval_expr(where_expr, row, &ctx).ok()?;
4285 if matches!(cond, Value::Bool(true)) {
4286 kept.push(i);
4287 if kept.len() >= limit {
4288 break;
4289 }
4290 }
4291 }
4292 Some(kept)
4293 } else {
4294 Some(spg_storage::nsw_query(
4295 table, &idx.name, &query, limit, metric,
4296 ))
4297 }
4298}
4299
4300const NSW_OVER_FETCH_FLOOR: usize = 32;
4304
4305fn literal_to_vector(e: &Expr) -> Option<Vec<f32>> {
4308 match e {
4309 Expr::Literal(Literal::Vector(v)) => Some(v.clone()),
4310 Expr::Cast { expr, .. } => literal_to_vector(expr),
4311 _ => None,
4312 }
4313}
4314
4315fn materialise_in_order(
4319 stmt: &SelectStatement,
4320 table: &Table,
4321 schema_cols: &[ColumnSchema],
4322 table_alias: &str,
4323 ordered_rows: &[usize],
4324) -> Result<QueryResult, EngineError> {
4325 let ctx = EvalContext::new(schema_cols, Some(table_alias));
4326 let projection = build_projection(&stmt.items, schema_cols, table_alias)?;
4327 let mut output_rows: Vec<Row> = Vec::with_capacity(ordered_rows.len());
4328 for &i in ordered_rows {
4329 let row = &table.rows()[i];
4330 let mut values = Vec::with_capacity(projection.len());
4331 for p in &projection {
4332 values.push(eval::eval_expr(&p.expr, row, &ctx)?);
4333 }
4334 output_rows.push(Row::new(values));
4335 }
4336 apply_offset_and_limit(&mut output_rows, stmt.offset, stmt.limit);
4337 let columns: Vec<ColumnSchema> = projection
4338 .into_iter()
4339 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
4340 .collect();
4341 Ok(QueryResult::Rows {
4342 columns,
4343 rows: output_rows,
4344 })
4345}
4346
4347fn try_index_seek<'a>(
4348 where_expr: &Expr,
4349 schema_cols: &[ColumnSchema],
4350 catalog: &'a Catalog,
4351 table: &'a Table,
4352 table_alias: &str,
4353) -> Option<Vec<Cow<'a, Row>>> {
4354 let Expr::Binary {
4355 lhs,
4356 op: BinOp::Eq,
4357 rhs,
4358 } = where_expr
4359 else {
4360 return None;
4361 };
4362 let (col_pos, value) = resolve_col_literal_pair(lhs, rhs, schema_cols, table_alias)
4363 .or_else(|| resolve_col_literal_pair(rhs, lhs, schema_cols, table_alias))?;
4364 let idx = table.index_on(col_pos)?;
4365 let key = IndexKey::from_value(&value)?;
4366 let locators = idx.lookup_eq(&key);
4367 let table_name = table.schema().name.as_str();
4368 let mut out: Vec<Cow<'a, Row>> = Vec::with_capacity(locators.len());
4376 for loc in locators {
4377 match *loc {
4378 spg_storage::RowLocator::Hot(i) => {
4379 if let Some(row) = table.rows().get(i) {
4380 out.push(Cow::Borrowed(row));
4381 }
4382 }
4383 spg_storage::RowLocator::Cold { segment_id, .. } => {
4384 if let Some(row) = catalog.resolve_cold_locator(table_name, segment_id, &key) {
4385 out.push(Cow::Owned(row));
4386 }
4387 }
4388 }
4389 }
4390 Some(out)
4391}
4392
4393fn try_pk_predicate(
4405 where_expr: &Expr,
4406 schema_cols: &[ColumnSchema],
4407 table_alias: &str,
4408) -> Option<(usize, IndexKey)> {
4409 let Expr::Binary {
4410 lhs,
4411 op: BinOp::Eq,
4412 rhs,
4413 } = where_expr
4414 else {
4415 return None;
4416 };
4417 let (col_pos, value) = resolve_col_literal_pair(lhs, rhs, schema_cols, table_alias)
4418 .or_else(|| resolve_col_literal_pair(rhs, lhs, schema_cols, table_alias))?;
4419 let key = IndexKey::from_value(&value)?;
4420 Some((col_pos, key))
4421}
4422
4423fn resolve_col_literal_pair(
4424 col_side: &Expr,
4425 lit_side: &Expr,
4426 schema_cols: &[ColumnSchema],
4427 table_alias: &str,
4428) -> Option<(usize, Value)> {
4429 let Expr::Column(c) = col_side else {
4430 return None;
4431 };
4432 if let Some(q) = &c.qualifier
4433 && q != table_alias
4434 {
4435 return None;
4436 }
4437 let pos = schema_cols.iter().position(|s| s.name == c.name)?;
4438 let Expr::Literal(l) = lit_side else {
4439 return None;
4440 };
4441 let v = match l {
4442 Literal::Integer(n) => {
4443 if let Ok(small) = i32::try_from(*n) {
4444 Value::Int(small)
4445 } else {
4446 Value::BigInt(*n)
4447 }
4448 }
4449 Literal::Float(x) => Value::Float(*x),
4450 Literal::String(s) => Value::Text(s.clone()),
4451 Literal::Bool(b) => Value::Bool(*b),
4452 Literal::Null => Value::Null,
4453 Literal::Vector(_) | Literal::Interval { .. } => return None,
4456 };
4457 Some((pos, v))
4458}
4459
4460fn resolve_projection_column<'a>(
4465 c: &ColumnName,
4466 schema_cols: &'a [ColumnSchema],
4467 table_alias: &str,
4468) -> Result<&'a ColumnSchema, EngineError> {
4469 if let Some(q) = &c.qualifier {
4470 let composite = alloc::format!("{q}.{name}", name = c.name);
4471 if let Some(s) = schema_cols.iter().find(|s| s.name == composite) {
4472 return Ok(s);
4473 }
4474 if q == table_alias
4477 && let Some(s) = schema_cols.iter().find(|s| s.name == c.name)
4478 {
4479 return Ok(s);
4480 }
4481 let prefix = alloc::format!("{q}.");
4485 let qualifier_known =
4486 q == table_alias || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
4487 if !qualifier_known {
4488 return Err(EngineError::Eval(EvalError::UnknownQualifier {
4489 qualifier: q.clone(),
4490 }));
4491 }
4492 return Err(EngineError::Eval(EvalError::ColumnNotFound {
4493 name: c.name.clone(),
4494 }));
4495 }
4496 if let Some(s) = schema_cols.iter().find(|s| s.name == c.name) {
4497 return Ok(s);
4498 }
4499 let suffix = alloc::format!(".{name}", name = c.name);
4500 let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
4501 let first = matches.next();
4502 let extra = matches.next();
4503 match (first, extra) {
4504 (Some(s), None) => Ok(s),
4505 (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
4506 detail: alloc::format!("ambiguous column reference: {}", c.name),
4507 })),
4508 _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
4509 name: c.name.clone(),
4510 })),
4511 }
4512}
4513
4514fn build_projection(
4515 items: &[SelectItem],
4516 schema_cols: &[ColumnSchema],
4517 table_alias: &str,
4518) -> Result<Vec<ProjectedItem>, EngineError> {
4519 let mut out = Vec::new();
4520 for item in items {
4521 match item {
4522 SelectItem::Wildcard => {
4523 for col in schema_cols {
4524 out.push(ProjectedItem {
4525 expr: Expr::Column(ColumnName {
4526 qualifier: None,
4527 name: col.name.clone(),
4528 }),
4529 output_name: col.name.clone(),
4530 ty: col.ty,
4531 nullable: col.nullable,
4532 });
4533 }
4534 }
4535 SelectItem::Expr { expr, alias } => {
4536 if let Expr::Column(c) = expr {
4541 let sch = resolve_projection_column(c, schema_cols, table_alias)?;
4542 let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
4543 out.push(ProjectedItem {
4544 expr: expr.clone(),
4545 output_name,
4546 ty: sch.ty,
4547 nullable: sch.nullable,
4548 });
4549 } else {
4550 let output_name = alias.clone().unwrap_or_else(|| expr.to_string());
4551 out.push(ProjectedItem {
4552 expr: expr.clone(),
4553 output_name,
4554 ty: DataType::Text,
4555 nullable: true,
4556 });
4557 }
4558 }
4559 }
4560 }
4561 Ok(out)
4562}
4563
4564fn numeric_from_integer(
4568 n: i128,
4569 precision: u8,
4570 scale: u8,
4571 col_name: &str,
4572) -> Result<Value, EngineError> {
4573 let factor = pow10_i128(scale);
4574 let scaled = n.checked_mul(factor).ok_or_else(|| {
4575 EngineError::Unsupported(alloc::format!(
4576 "integer overflow scaling value for column `{col_name}` to scale {scale}"
4577 ))
4578 })?;
4579 check_precision(scaled, precision, col_name)?;
4580 Ok(Value::Numeric { scaled, scale })
4581}
4582
4583#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
4586fn numeric_from_float(
4587 x: f64,
4588 precision: u8,
4589 scale: u8,
4590 col_name: &str,
4591) -> Result<Value, EngineError> {
4592 if !x.is_finite() {
4593 return Err(EngineError::Unsupported(alloc::format!(
4594 "cannot store non-finite float in NUMERIC column `{col_name}`"
4595 )));
4596 }
4597 let mut factor = 1.0_f64;
4598 for _ in 0..scale {
4599 factor *= 10.0;
4600 }
4601 let shifted = x * factor;
4606 let biased = if shifted >= 0.0 {
4607 shifted + 0.5
4608 } else {
4609 shifted - 0.5
4610 };
4611 if !(-1e38..=1e38).contains(&biased) {
4614 return Err(EngineError::Unsupported(alloc::format!(
4615 "value {x} overflows NUMERIC range for column `{col_name}`"
4616 )));
4617 }
4618 let scaled = biased as i128;
4619 check_precision(scaled, precision, col_name)?;
4620 Ok(Value::Numeric { scaled, scale })
4621}
4622
4623fn numeric_rescale(
4626 scaled: i128,
4627 src_scale: u8,
4628 precision: u8,
4629 dst_scale: u8,
4630 col_name: &str,
4631) -> Result<Value, EngineError> {
4632 let new_scaled = if dst_scale >= src_scale {
4633 let bump = pow10_i128(dst_scale - src_scale);
4634 scaled.checked_mul(bump).ok_or_else(|| {
4635 EngineError::Unsupported(alloc::format!(
4636 "overflow rescaling NUMERIC for column `{col_name}`"
4637 ))
4638 })?
4639 } else {
4640 let drop = pow10_i128(src_scale - dst_scale);
4641 let half = drop / 2;
4642 if scaled >= 0 {
4643 (scaled + half) / drop
4644 } else {
4645 (scaled - half) / drop
4646 }
4647 };
4648 check_precision(new_scaled, precision, col_name)?;
4649 Ok(Value::Numeric {
4650 scaled: new_scaled,
4651 scale: dst_scale,
4652 })
4653}
4654
4655const fn numeric_truncate_to_integer(scaled: i128, scale: u8) -> i128 {
4658 if scale == 0 {
4659 return scaled;
4660 }
4661 let factor = pow10_i128_const(scale);
4662 scaled / factor
4663}
4664
4665fn check_precision(scaled: i128, precision: u8, col_name: &str) -> Result<(), EngineError> {
4669 if precision == 0 {
4670 return Ok(());
4671 }
4672 let limit = pow10_i128(precision);
4673 if scaled.unsigned_abs() >= limit.unsigned_abs() {
4674 return Err(EngineError::Unsupported(alloc::format!(
4675 "NUMERIC value exceeds precision {precision} for column `{col_name}`"
4676 )));
4677 }
4678 Ok(())
4679}
4680
4681const fn pow10_i128_const(p: u8) -> i128 {
4682 let mut acc: i128 = 1;
4683 let mut i = 0;
4684 while i < p {
4685 acc *= 10;
4686 i += 1;
4687 }
4688 acc
4689}
4690
4691fn pow10_i128(p: u8) -> i128 {
4692 pow10_i128_const(p)
4693}
4694
4695impl Engine {
4710 #[allow(
4721 clippy::too_many_lines,
4722 clippy::type_complexity,
4723 clippy::needless_range_loop
4724 )] fn exec_select_with_window(
4726 &self,
4727 stmt: &SelectStatement,
4728 cancel: CancelToken<'_>,
4729 ) -> Result<QueryResult, EngineError> {
4730 let from = stmt.from.as_ref().ok_or_else(|| {
4731 EngineError::Unsupported("window functions require a FROM clause".into())
4732 })?;
4733 if !from.joins.is_empty() {
4736 return Err(EngineError::Unsupported(
4737 "JOIN with window functions not yet supported".into(),
4738 ));
4739 }
4740 let primary = &from.primary;
4741 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4742 StorageError::TableNotFound {
4743 name: primary.name.clone(),
4744 }
4745 })?;
4746 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4747 let schema_cols = &table.schema().columns;
4748 let ctx = EvalContext::new(schema_cols, Some(alias));
4749
4750 let mut filtered: Vec<&Row> = Vec::new();
4752 for (i, row) in table.rows().iter().enumerate() {
4753 if i.is_multiple_of(256) {
4754 cancel.check()?;
4755 }
4756 if let Some(w) = &stmt.where_ {
4757 let cond = eval::eval_expr(w, row, &ctx)?;
4758 if !matches!(cond, Value::Bool(true)) {
4759 continue;
4760 }
4761 }
4762 filtered.push(row);
4763 }
4764 let n_rows = filtered.len();
4765
4766 let mut window_nodes: Vec<Expr> = Vec::new();
4768 for item in &stmt.items {
4769 if let SelectItem::Expr { expr, .. } = item {
4770 collect_window_nodes(expr, &mut window_nodes);
4771 }
4772 }
4773
4774 let mut win_vals: Vec<Vec<Value>> = Vec::with_capacity(window_nodes.len());
4777 for wnode in &window_nodes {
4778 let Expr::WindowFunction {
4779 name,
4780 args,
4781 partition_by,
4782 order_by,
4783 frame,
4784 null_treatment,
4785 } = wnode
4786 else {
4787 unreachable!("collect_window_nodes pushes only WindowFunction");
4788 };
4789 let mut indexed: Vec<(Vec<Value>, Vec<(Value, bool)>, usize)> =
4791 Vec::with_capacity(n_rows);
4792 for (i, row) in filtered.iter().enumerate() {
4793 let pkey: Vec<Value> = partition_by
4794 .iter()
4795 .map(|p| eval::eval_expr(p, row, &ctx))
4796 .collect::<Result<_, _>>()?;
4797 let okey: Vec<(Value, bool)> = order_by
4798 .iter()
4799 .map(|(e, desc)| eval::eval_expr(e, row, &ctx).map(|v| (v, *desc)))
4800 .collect::<Result<_, _>>()?;
4801 indexed.push((pkey, okey, i));
4802 }
4803 indexed.sort_by(|a, b| {
4806 let p_cmp = partition_key_cmp(&a.0, &b.0);
4807 if p_cmp != core::cmp::Ordering::Equal {
4808 return p_cmp;
4809 }
4810 order_key_cmp(&a.1, &b.1)
4811 });
4812 let mut out_vals: Vec<Value> = alloc::vec![Value::Null; n_rows];
4814 let mut p_start = 0;
4815 while p_start < indexed.len() {
4816 let mut p_end = p_start + 1;
4817 while p_end < indexed.len()
4818 && partition_key_cmp(&indexed[p_start].0, &indexed[p_end].0)
4819 == core::cmp::Ordering::Equal
4820 {
4821 p_end += 1;
4822 }
4823 compute_window_partition(
4825 name,
4826 args,
4827 !order_by.is_empty(),
4828 frame.as_ref(),
4829 *null_treatment,
4830 &indexed[p_start..p_end],
4831 &filtered,
4832 &ctx,
4833 &mut out_vals,
4834 )?;
4835 p_start = p_end;
4836 }
4837 win_vals.push(out_vals);
4838 }
4839
4840 let mut ext_cols = schema_cols.clone();
4842 for i in 0..window_nodes.len() {
4843 ext_cols.push(ColumnSchema::new(
4844 alloc::format!("__win_{i}"),
4845 DataType::Text, true,
4847 ));
4848 }
4849 let mut ext_rows: Vec<Row> = Vec::with_capacity(n_rows);
4851 for i in 0..n_rows {
4852 let mut values = filtered[i].values.clone();
4853 for w in 0..window_nodes.len() {
4854 values.push(win_vals[w][i].clone());
4855 }
4856 ext_rows.push(Row::new(values));
4857 }
4858 let mut rewritten_items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
4860 for item in &stmt.items {
4861 let new_item = match item {
4862 SelectItem::Wildcard => SelectItem::Wildcard,
4863 SelectItem::Expr { expr, alias } => {
4864 let mut e = expr.clone();
4865 rewrite_window_to_columns(&mut e, &window_nodes);
4866 SelectItem::Expr {
4867 expr: e,
4868 alias: alias.clone(),
4869 }
4870 }
4871 };
4872 rewritten_items.push(new_item);
4873 }
4874
4875 let ext_ctx = EvalContext::new(&ext_cols, Some(alias));
4877 let projection = build_projection(&rewritten_items, &ext_cols, alias)?;
4878 let mut tagged: Vec<(Vec<f64>, Row)> = Vec::with_capacity(n_rows);
4879 for (i, row) in ext_rows.iter().enumerate() {
4880 if i.is_multiple_of(256) {
4881 cancel.check()?;
4882 }
4883 let mut values = Vec::with_capacity(projection.len());
4884 for p in &projection {
4885 values.push(eval::eval_expr(&p.expr, row, &ext_ctx)?);
4886 }
4887 let order_keys = if stmt.order_by.is_empty() {
4888 Vec::new()
4889 } else {
4890 let mut keys = Vec::with_capacity(stmt.order_by.len());
4891 for o in &stmt.order_by {
4892 let mut e = o.expr.clone();
4893 rewrite_window_to_columns(&mut e, &window_nodes);
4894 let key = eval::eval_expr(&e, row, &ext_ctx)?;
4895 keys.push(value_to_order_key(&key)?);
4896 }
4897 keys
4898 };
4899 tagged.push((order_keys, Row::new(values)));
4900 }
4901 if !stmt.order_by.is_empty() {
4903 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
4904 sort_by_keys(&mut tagged, &descs);
4905 }
4906 let mut out_rows: Vec<Row> = tagged.into_iter().map(|(_, r)| r).collect();
4907 apply_offset_and_limit(&mut out_rows, stmt.offset, stmt.limit);
4908 let final_cols: Vec<ColumnSchema> = projection
4909 .into_iter()
4910 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
4911 .collect();
4912 Ok(QueryResult::Rows {
4913 columns: final_cols,
4914 rows: out_rows,
4915 })
4916 }
4917
4918 fn exec_with_ctes(
4925 &self,
4926 stmt: &SelectStatement,
4927 cancel: CancelToken<'_>,
4928 ) -> Result<QueryResult, EngineError> {
4929 cancel.check()?;
4930 let mut catalog = self.active_catalog().clone();
4931 for cte in &stmt.ctes {
4932 if catalog.get(&cte.name).is_some() {
4933 return Err(EngineError::Unsupported(alloc::format!(
4934 "CTE name {:?} shadows an existing table; rename the CTE",
4935 cte.name
4936 )));
4937 }
4938 let (columns, rows) = if cte.recursive {
4939 self.materialise_recursive_cte(cte, &catalog, cancel)?
4940 } else {
4941 let body_result = self.exec_select_cancel(&cte.body, cancel)?;
4942 let QueryResult::Rows { columns, rows } = body_result else {
4943 return Err(EngineError::Unsupported(alloc::format!(
4944 "CTE {:?} body did not return rows",
4945 cte.name
4946 )));
4947 };
4948 (columns, rows)
4949 };
4950 let inferred = infer_column_types(&columns, &rows);
4955 let mut columns = inferred;
4956 if !cte.column_overrides.is_empty() {
4958 if cte.column_overrides.len() != columns.len() {
4959 return Err(EngineError::Unsupported(alloc::format!(
4960 "CTE {:?} column list has {} names but body returns {} columns",
4961 cte.name,
4962 cte.column_overrides.len(),
4963 columns.len()
4964 )));
4965 }
4966 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
4967 col.name.clone_from(name);
4968 }
4969 }
4970 let schema = TableSchema::new(cte.name.clone(), columns);
4971 catalog.create_table(schema).map_err(EngineError::Storage)?;
4972 let table = catalog
4973 .get_mut(&cte.name)
4974 .expect("just-created CTE table must exist");
4975 for row in rows {
4976 table.insert(row).map_err(EngineError::Storage)?;
4977 }
4978 }
4979 let mut body = stmt.clone();
4982 body.ctes = Vec::new();
4983 let mut temp = Engine::restore(catalog);
4984 if let Some(c) = self.clock {
4985 temp = temp.with_clock(c);
4986 }
4987 if let Some(f) = self.salt_fn {
4988 temp = temp.with_salt_fn(f);
4989 }
4990 temp.exec_select_cancel(&body, cancel)
4991 }
4992
4993 #[allow(clippy::too_many_lines)]
5003 fn materialise_recursive_cte(
5004 &self,
5005 cte: &spg_sql::ast::Cte,
5006 base_catalog: &Catalog,
5007 cancel: CancelToken<'_>,
5008 ) -> Result<(Vec<ColumnSchema>, Vec<Row>), EngineError> {
5009 const MAX_TOTAL_ROWS: usize = 1_000_000;
5010 const MAX_ITERATIONS: usize = 100_000;
5011 cancel.check()?;
5012 if cte.body.unions.is_empty() {
5013 return Err(EngineError::Unsupported(alloc::format!(
5014 "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
5015 cte.name
5016 )));
5017 }
5018 let mut anchor = cte.body.clone();
5020 let union_terms = core::mem::take(&mut anchor.unions);
5021 anchor.ctes = Vec::new();
5022 if select_refers_to(&anchor, &cte.name) {
5024 return Err(EngineError::Unsupported(alloc::format!(
5025 "WITH RECURSIVE {:?}: the anchor must not reference the CTE itself",
5026 cte.name
5027 )));
5028 }
5029 let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
5030 let QueryResult::Rows {
5031 columns: anchor_cols,
5032 rows: anchor_rows,
5033 } = anchor_result
5034 else {
5035 return Err(EngineError::Unsupported(alloc::format!(
5036 "WITH RECURSIVE {:?}: anchor did not return rows",
5037 cte.name
5038 )));
5039 };
5040 let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
5044 if !cte.column_overrides.is_empty() {
5045 if cte.column_overrides.len() != columns.len() {
5046 return Err(EngineError::Unsupported(alloc::format!(
5047 "CTE {:?} column list has {} names but anchor returns {} columns",
5048 cte.name,
5049 cte.column_overrides.len(),
5050 columns.len()
5051 )));
5052 }
5053 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
5054 col.name.clone_from(name);
5055 }
5056 }
5057 let mut all_rows: Vec<Row> = anchor_rows.clone();
5058 let mut working_set: Vec<Row> = anchor_rows;
5059 let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
5060 let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
5063 if !all_union_all {
5064 for r in &all_rows {
5065 seen.insert(encode_row_key(r));
5066 }
5067 }
5068 for iter in 0..MAX_ITERATIONS {
5069 cancel.check()?;
5070 if working_set.is_empty() {
5071 break;
5072 }
5073 let mut iter_catalog = base_catalog.clone();
5075 let schema = TableSchema::new(cte.name.clone(), columns.clone());
5076 iter_catalog
5077 .create_table(schema)
5078 .map_err(EngineError::Storage)?;
5079 {
5080 let table = iter_catalog.get_mut(&cte.name).expect("just-created");
5081 for row in &working_set {
5082 table.insert(row.clone()).map_err(EngineError::Storage)?;
5083 }
5084 }
5085 let mut iter_engine = Engine::restore(iter_catalog);
5086 if let Some(c) = self.clock {
5087 iter_engine = iter_engine.with_clock(c);
5088 }
5089 if let Some(f) = self.salt_fn {
5090 iter_engine = iter_engine.with_salt_fn(f);
5091 }
5092 let mut next_set: Vec<Row> = Vec::new();
5094 for (_, term) in &union_terms {
5095 let mut term = term.clone();
5096 term.ctes = Vec::new();
5097 let r = iter_engine.exec_select_cancel(&term, cancel)?;
5098 let QueryResult::Rows {
5099 columns: rc,
5100 rows: rs,
5101 } = r
5102 else {
5103 return Err(EngineError::Unsupported(alloc::format!(
5104 "WITH RECURSIVE {:?}: recursive term did not return rows",
5105 cte.name
5106 )));
5107 };
5108 if rc.len() != columns.len() {
5109 return Err(EngineError::Unsupported(alloc::format!(
5110 "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
5111 cte.name,
5112 rc.len(),
5113 columns.len()
5114 )));
5115 }
5116 for row in rs {
5117 if !all_union_all {
5118 let key = encode_row_key(&row);
5119 if !seen.insert(key) {
5120 continue;
5121 }
5122 }
5123 next_set.push(row);
5124 }
5125 }
5126 if next_set.is_empty() {
5127 break;
5128 }
5129 all_rows.extend(next_set.iter().cloned());
5130 working_set = next_set;
5131 if all_rows.len() > MAX_TOTAL_ROWS {
5132 return Err(EngineError::Unsupported(alloc::format!(
5133 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
5134 cte.name
5135 )));
5136 }
5137 if iter + 1 == MAX_ITERATIONS {
5138 return Err(EngineError::Unsupported(alloc::format!(
5139 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
5140 cte.name
5141 )));
5142 }
5143 }
5144 Ok((columns, all_rows))
5145 }
5146
5147 fn resolve_select_subqueries(
5148 &self,
5149 stmt: &mut SelectStatement,
5150 cancel: CancelToken<'_>,
5151 ) -> Result<(), EngineError> {
5152 for item in &mut stmt.items {
5153 if let SelectItem::Expr { expr, .. } = item {
5154 self.resolve_expr_subqueries(expr, cancel)?;
5155 }
5156 }
5157 if let Some(w) = &mut stmt.where_ {
5158 self.resolve_expr_subqueries(w, cancel)?;
5159 }
5160 if let Some(gs) = &mut stmt.group_by {
5161 for g in gs {
5162 self.resolve_expr_subqueries(g, cancel)?;
5163 }
5164 }
5165 if let Some(h) = &mut stmt.having {
5166 self.resolve_expr_subqueries(h, cancel)?;
5167 }
5168 for o in &mut stmt.order_by {
5169 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
5170 }
5171 for (_, peer) in &mut stmt.unions {
5172 self.resolve_select_subqueries(peer, cancel)?;
5173 }
5174 Ok(())
5175 }
5176
5177 #[allow(clippy::only_used_in_recursion)] fn resolve_expr_subqueries(
5179 &self,
5180 e: &mut Expr,
5181 cancel: CancelToken<'_>,
5182 ) -> Result<(), EngineError> {
5183 if let Some(replacement) = self.subquery_replacement(e, cancel)? {
5185 *e = replacement;
5186 return Ok(());
5187 }
5188 match e {
5189 Expr::Binary { lhs, rhs, .. } => {
5190 self.resolve_expr_subqueries(lhs, cancel)?;
5191 self.resolve_expr_subqueries(rhs, cancel)?;
5192 }
5193 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
5194 self.resolve_expr_subqueries(expr, cancel)?;
5195 }
5196 Expr::FunctionCall { args, .. } => {
5197 for a in args {
5198 self.resolve_expr_subqueries(a, cancel)?;
5199 }
5200 }
5201 Expr::Like { expr, pattern, .. } => {
5202 self.resolve_expr_subqueries(expr, cancel)?;
5203 self.resolve_expr_subqueries(pattern, cancel)?;
5204 }
5205 Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
5206 Expr::WindowFunction {
5209 args,
5210 partition_by,
5211 order_by,
5212 ..
5213 } => {
5214 for a in args {
5215 self.resolve_expr_subqueries(a, cancel)?;
5216 }
5217 for p in partition_by {
5218 self.resolve_expr_subqueries(p, cancel)?;
5219 }
5220 for (e, _) in order_by {
5221 self.resolve_expr_subqueries(e, cancel)?;
5222 }
5223 }
5224 Expr::ScalarSubquery(_)
5228 | Expr::Exists { .. }
5229 | Expr::InSubquery { .. }
5230 | Expr::Literal(_)
5231 | Expr::Placeholder(_)
5232 | Expr::Column(_) => {}
5233 }
5234 Ok(())
5235 }
5236
5237 fn eval_expr_with_correlated(
5245 &self,
5246 expr: &Expr,
5247 row: &Row,
5248 ctx: &EvalContext<'_>,
5249 cancel: CancelToken<'_>,
5250 memo: Option<&mut memoize::MemoizeCache>,
5251 ) -> Result<Value, EngineError> {
5252 if !expr_has_subquery(expr) {
5253 return eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval);
5254 }
5255 let mut e = expr.clone();
5256 self.resolve_correlated_in_expr(&mut e, row, ctx, cancel, memo)?;
5257 eval::eval_expr(&e, row, ctx).map_err(EngineError::Eval)
5258 }
5259
5260 fn resolve_correlated_in_expr(
5261 &self,
5262 e: &mut Expr,
5263 row: &Row,
5264 ctx: &EvalContext<'_>,
5265 cancel: CancelToken<'_>,
5266 mut memo: Option<&mut memoize::MemoizeCache>,
5267 ) -> Result<(), EngineError> {
5268 match e {
5269 Expr::ScalarSubquery(inner) => {
5270 let cache_key = memo.as_ref().map(|_| memoize::CacheKey {
5275 subquery_repr: alloc::format!("{}", **inner),
5276 outer_values: row.values.clone(),
5277 });
5278 if let (Some(cache), Some(k)) = (memo.as_deref_mut(), cache_key.as_ref())
5279 && let Some(cached) = cache.get(k)
5280 {
5281 *e = value_to_literal_expr(cached)?;
5282 return Ok(());
5283 }
5284 let mut s = (**inner).clone();
5285 substitute_outer_columns(&mut s, row, ctx);
5286 let r = self.exec_select_cancel(&s, cancel)?;
5287 let QueryResult::Rows { rows, .. } = r else {
5288 return Err(EngineError::Unsupported(
5289 "scalar subquery: inner did not return rows".into(),
5290 ));
5291 };
5292 let value = match rows.as_slice() {
5293 [] => Value::Null,
5294 [r0] => r0.values.first().cloned().unwrap_or(Value::Null),
5295 _ => {
5296 return Err(EngineError::Unsupported(alloc::format!(
5297 "scalar subquery returned {} rows; expected 0 or 1",
5298 rows.len()
5299 )));
5300 }
5301 };
5302 if let (Some(cache), Some(k)) = (memo.as_deref_mut(), cache_key) {
5303 cache.insert(k, value.clone());
5304 }
5305 *e = value_to_literal_expr(value)?;
5306 }
5307 Expr::Exists { subquery, negated } => {
5308 let mut s = (**subquery).clone();
5309 substitute_outer_columns(&mut s, row, ctx);
5310 let r = self.exec_select_cancel(&s, cancel)?;
5311 let exists = matches!(r, QueryResult::Rows { rows, .. } if !rows.is_empty());
5312 let bit = if *negated { !exists } else { exists };
5313 *e = Expr::Literal(Literal::Bool(bit));
5314 }
5315 Expr::InSubquery {
5316 expr: lhs,
5317 subquery,
5318 negated,
5319 } => {
5320 self.resolve_correlated_in_expr(lhs, row, ctx, cancel, memo.as_deref_mut())?;
5321 let lhs_val = eval::eval_expr(lhs, row, ctx).map_err(EngineError::Eval)?;
5322 let mut s = (**subquery).clone();
5323 substitute_outer_columns(&mut s, row, ctx);
5324 let r = self.exec_select_cancel(&s, cancel)?;
5325 let QueryResult::Rows { columns, rows, .. } = r else {
5326 return Err(EngineError::Unsupported(
5327 "IN-subquery: inner did not return rows".into(),
5328 ));
5329 };
5330 if columns.len() != 1 {
5331 return Err(EngineError::Unsupported(alloc::format!(
5332 "IN-subquery must project exactly one column; got {}",
5333 columns.len()
5334 )));
5335 }
5336 let mut found = false;
5337 let mut any_null = false;
5338 for r0 in rows {
5339 let v = r0.values.into_iter().next().unwrap_or(Value::Null);
5340 if v.is_null() {
5341 any_null = true;
5342 continue;
5343 }
5344 if value_cmp(&v, &lhs_val) == core::cmp::Ordering::Equal {
5345 found = true;
5346 break;
5347 }
5348 }
5349 let bit = if found {
5350 !*negated
5351 } else if any_null {
5352 return Err(EngineError::Unsupported(
5353 "IN-subquery with NULL in result and no match: NULL semantics not yet implemented".into(),
5354 ));
5355 } else {
5356 *negated
5357 };
5358 *e = Expr::Literal(Literal::Bool(bit));
5359 }
5360 Expr::Binary { lhs, rhs, .. } => {
5361 self.resolve_correlated_in_expr(lhs, row, ctx, cancel, memo.as_deref_mut())?;
5362 self.resolve_correlated_in_expr(rhs, row, ctx, cancel, memo.as_deref_mut())?;
5363 }
5364 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
5365 self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
5366 }
5367 Expr::Like { expr, pattern, .. } => {
5368 self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
5369 self.resolve_correlated_in_expr(pattern, row, ctx, cancel, memo.as_deref_mut())?;
5370 }
5371 Expr::FunctionCall { args, .. } => {
5372 for a in args {
5373 self.resolve_correlated_in_expr(a, row, ctx, cancel, memo.as_deref_mut())?;
5374 }
5375 }
5376 Expr::Extract { source, .. } => {
5377 self.resolve_correlated_in_expr(source, row, ctx, cancel, memo.as_deref_mut())?;
5378 }
5379 Expr::WindowFunction { .. } | Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {}
5380 }
5381 Ok(())
5382 }
5383
5384 fn subquery_replacement(
5385 &self,
5386 e: &Expr,
5387 cancel: CancelToken<'_>,
5388 ) -> Result<Option<Expr>, EngineError> {
5389 match e {
5390 Expr::ScalarSubquery(inner) => {
5391 let mut s = (**inner).clone();
5392 self.resolve_select_subqueries(&mut s, cancel)?;
5395 let r = match self.exec_bare_select_cancel(&s, cancel) {
5396 Ok(r) => r,
5397 Err(e) if is_correlation_error(&e) => return Ok(None),
5398 Err(e) => return Err(e),
5399 };
5400 let QueryResult::Rows { rows, .. } = r else {
5401 return Err(EngineError::Unsupported(
5402 "scalar subquery: inner statement did not return rows".into(),
5403 ));
5404 };
5405 let value = match rows.as_slice() {
5406 [] => Value::Null,
5407 [row] => row.values.first().cloned().unwrap_or(Value::Null),
5408 _ => {
5409 return Err(EngineError::Unsupported(alloc::format!(
5410 "scalar subquery returned {} rows; expected 0 or 1",
5411 rows.len()
5412 )));
5413 }
5414 };
5415 Ok(Some(value_to_literal_expr(value)?))
5416 }
5417 Expr::Exists { subquery, negated } => {
5418 let mut s = (**subquery).clone();
5419 self.resolve_select_subqueries(&mut s, cancel)?;
5420 let r = match self.exec_bare_select_cancel(&s, cancel) {
5421 Ok(r) => r,
5422 Err(e) if is_correlation_error(&e) => return Ok(None),
5423 Err(e) => return Err(e),
5424 };
5425 let exists = match r {
5426 QueryResult::Rows { rows, .. } => !rows.is_empty(),
5427 QueryResult::CommandOk { .. } => false,
5428 };
5429 let bit = if *negated { !exists } else { exists };
5430 Ok(Some(Expr::Literal(Literal::Bool(bit))))
5431 }
5432 Expr::InSubquery {
5433 expr,
5434 subquery,
5435 negated,
5436 } => {
5437 let mut s = (**subquery).clone();
5438 self.resolve_select_subqueries(&mut s, cancel)?;
5439 let r = match self.exec_bare_select_cancel(&s, cancel) {
5440 Ok(r) => r,
5441 Err(e) if is_correlation_error(&e) => return Ok(None),
5442 Err(e) => return Err(e),
5443 };
5444 let QueryResult::Rows { columns, rows, .. } = r else {
5445 return Err(EngineError::Unsupported(
5446 "IN-subquery: inner statement did not return rows".into(),
5447 ));
5448 };
5449 if columns.len() != 1 {
5450 return Err(EngineError::Unsupported(alloc::format!(
5451 "IN-subquery must project exactly one column; got {}",
5452 columns.len()
5453 )));
5454 }
5455 let mut acc: Option<Expr> = None;
5458 for row in rows {
5459 let v = row.values.into_iter().next().unwrap_or(Value::Null);
5460 let lit = value_to_literal_expr(v)?;
5461 let cmp = Expr::Binary {
5462 lhs: expr.clone(),
5463 op: BinOp::Eq,
5464 rhs: Box::new(lit),
5465 };
5466 acc = Some(match acc {
5467 None => cmp,
5468 Some(prev) => Expr::Binary {
5469 lhs: Box::new(prev),
5470 op: BinOp::Or,
5471 rhs: Box::new(cmp),
5472 },
5473 });
5474 }
5475 let combined = acc.unwrap_or(Expr::Literal(Literal::Bool(false)));
5476 let final_expr = if *negated {
5477 Expr::Unary {
5478 op: UnOp::Not,
5479 expr: Box::new(combined),
5480 }
5481 } else {
5482 combined
5483 };
5484 Ok(Some(final_expr))
5485 }
5486 _ => Ok(None),
5487 }
5488 }
5489}
5490
5491fn select_refers_to(stmt: &SelectStatement, target: &str) -> bool {
5503 if let Some(from) = &stmt.from
5504 && from_refers_to(from, target)
5505 {
5506 return true;
5507 }
5508 for (_, peer) in &stmt.unions {
5509 if select_refers_to(peer, target) {
5510 return true;
5511 }
5512 }
5513 for item in &stmt.items {
5514 if let SelectItem::Expr { expr, .. } = item
5515 && expr_refers_to(expr, target)
5516 {
5517 return true;
5518 }
5519 }
5520 if let Some(w) = &stmt.where_
5521 && expr_refers_to(w, target)
5522 {
5523 return true;
5524 }
5525 false
5526}
5527
5528fn from_refers_to(from: &FromClause, target: &str) -> bool {
5529 if from.primary.name.eq_ignore_ascii_case(target) {
5530 return true;
5531 }
5532 from.joins
5533 .iter()
5534 .any(|j| j.table.name.eq_ignore_ascii_case(target))
5535}
5536
5537fn expr_refers_to(e: &Expr, target: &str) -> bool {
5538 match e {
5539 Expr::ScalarSubquery(s) => select_refers_to(s, target),
5540 Expr::Exists { subquery, .. } | Expr::InSubquery { subquery, .. } => {
5541 select_refers_to(subquery, target)
5542 }
5543 Expr::Binary { lhs, rhs, .. } => expr_refers_to(lhs, target) || expr_refers_to(rhs, target),
5544 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
5545 expr_refers_to(expr, target)
5546 }
5547 Expr::Like { expr, pattern, .. } => {
5548 expr_refers_to(expr, target) || expr_refers_to(pattern, target)
5549 }
5550 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refers_to(a, target)),
5551 Expr::Extract { source, .. } => expr_refers_to(source, target),
5552 Expr::WindowFunction {
5553 args,
5554 partition_by,
5555 order_by,
5556 ..
5557 } => {
5558 args.iter().any(|a| expr_refers_to(a, target))
5559 || partition_by.iter().any(|p| expr_refers_to(p, target))
5560 || order_by.iter().any(|(o, _)| expr_refers_to(o, target))
5561 }
5562 Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => false,
5563 }
5564}
5565
5566fn infer_column_types(columns: &[ColumnSchema], rows: &[Row]) -> Vec<ColumnSchema> {
5572 let mut out = columns.to_vec();
5573 for (col_idx, col) in out.iter_mut().enumerate() {
5574 if col.ty != DataType::Text {
5575 continue;
5576 }
5577 let mut inferred: Option<DataType> = None;
5578 let mut all_null = true;
5579 for row in rows {
5580 let Some(v) = row.values.get(col_idx) else {
5581 continue;
5582 };
5583 let ty = match v {
5584 Value::Null => continue,
5585 Value::SmallInt(_) => DataType::SmallInt,
5586 Value::Int(_) => DataType::Int,
5587 Value::BigInt(_) => DataType::BigInt,
5588 Value::Float(_) => DataType::Float,
5589 Value::Bool(_) => DataType::Bool,
5590 Value::Vector(_) => DataType::Vector {
5591 dim: 0,
5592 encoding: VecEncoding::F32,
5593 },
5594 _ => DataType::Text,
5595 };
5596 all_null = false;
5597 inferred = Some(match inferred {
5598 None => ty,
5599 Some(prev) if prev == ty => prev,
5600 Some(_) => DataType::Text,
5601 });
5602 }
5603 if let Some(t) = inferred {
5604 col.ty = t;
5605 col.nullable = true;
5606 } else if all_null {
5607 col.nullable = true;
5608 }
5609 }
5610 out
5611}
5612
5613#[allow(clippy::too_many_lines, clippy::format_push_string)]
5618fn build_index_suggestions(stmt: &SelectStatement, engine: &Engine) -> Vec<String> {
5635 use alloc::collections::BTreeSet;
5636 let mut seen: BTreeSet<(String, String)> = BTreeSet::new();
5637 let mut out: Vec<String> = Vec::new();
5638 let cat = engine.active_catalog();
5639 let Some(from) = &stmt.from else {
5643 return out;
5644 };
5645 let mut tables: Vec<String> = Vec::new();
5646 tables.push(from.primary.name.clone());
5647 for j in &from.joins {
5648 tables.push(j.table.name.clone());
5649 }
5650 let mut col_refs: Vec<spg_sql::ast::ColumnName> = Vec::new();
5653 if let Some(w) = &stmt.where_ {
5654 collect_column_refs(w, &mut col_refs);
5655 }
5656 for j in &from.joins {
5657 if let Some(on) = &j.on {
5658 collect_column_refs(on, &mut col_refs);
5659 }
5660 }
5661 for cn in &col_refs {
5662 let owner: Option<String> = if let Some(q) = &cn.qualifier {
5665 tables.iter().find(|t| t == &q).cloned()
5666 } else {
5667 tables.iter().find_map(|t| {
5668 cat.get(t).and_then(|tbl| {
5669 if tbl.schema().column_position(&cn.name).is_some() {
5670 Some(t.clone())
5671 } else {
5672 None
5673 }
5674 })
5675 })
5676 };
5677 let Some(owner) = owner else {
5678 continue;
5679 };
5680 let Some(tbl) = cat.get(&owner) else {
5681 continue;
5682 };
5683 let Some(col_pos) = tbl.schema().column_position(&cn.name) else {
5684 continue;
5685 };
5686 let already_indexed = tbl.indices().iter().any(|i| {
5689 matches!(i.kind, spg_storage::IndexKind::BTree(_))
5690 && i.column_position == col_pos
5691 && i.expression.is_none()
5692 && i.partial_predicate.is_none()
5693 });
5694 if already_indexed {
5695 continue;
5696 }
5697 if seen.insert((owner.clone(), cn.name.clone())) {
5698 out.push(alloc::format!(
5699 "SUGGEST: CREATE INDEX ix_{}_{} ON {} ({})",
5700 owner,
5701 cn.name,
5702 owner,
5703 cn.name
5704 ));
5705 }
5706 }
5707 out
5708}
5709
5710fn collect_column_refs(expr: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
5713 match expr {
5714 Expr::Column(cn) => out.push(cn.clone()),
5715 Expr::FunctionCall { args, .. } => {
5716 for a in args {
5717 collect_column_refs(a, out);
5718 }
5719 }
5720 Expr::Binary { lhs, rhs, .. } => {
5721 collect_column_refs(lhs, out);
5722 collect_column_refs(rhs, out);
5723 }
5724 Expr::Unary { expr: e, .. } => collect_column_refs(e, out),
5725 _ => {}
5726 }
5727}
5728
5729fn annotate_explain_lines(lines: &mut [String], total_rows: usize, engine: &Engine) {
5730 let catalog = engine.active_catalog();
5731 let cold_ids = catalog.cold_segment_ids_global();
5732 let any_cold = !cold_ids.is_empty();
5733 let cold_ids_repr = if any_cold {
5734 let mut s = alloc::string::String::from("[");
5735 for (i, id) in cold_ids.iter().enumerate() {
5736 if i > 0 {
5737 s.push(',');
5738 }
5739 s.push_str(&alloc::format!("{id}"));
5740 }
5741 s.push(']');
5742 s
5743 } else {
5744 alloc::string::String::new()
5745 };
5746 for (idx, line) in lines.iter_mut().enumerate() {
5747 let trimmed = line.trim_start();
5748 let is_top_level = idx == 0;
5749 if is_top_level {
5750 line.push_str(&alloc::format!(" (rows={total_rows})"));
5751 continue;
5752 }
5753 if let Some(rest) = trimmed.strip_prefix("From: ") {
5754 let (name, scan_kind) = match rest.split_once(" [") {
5755 Some((n, k)) => (n.trim(), k.trim_end_matches(']')),
5756 None => (rest.trim(), ""),
5757 };
5758 let bare = name.split_whitespace().next().unwrap_or(name);
5759 let hot = catalog.get(bare).map(|t| t.rows().len());
5760 let annot = match (hot, scan_kind) {
5765 (Some(h), "full scan") => {
5766 let mut s = alloc::format!(" (hot_rows={h}");
5767 if any_cold {
5768 s.push_str(&alloc::format!(
5769 ", cold_tier=present, cold_segments={cold_ids_repr}"
5770 ));
5771 }
5772 s.push(')');
5773 s
5774 }
5775 (Some(h), "index seek") => {
5776 let mut s = alloc::format!(" (hot_rows≤{h}");
5777 if any_cold {
5778 s.push_str(&alloc::format!(
5779 ", cold_tier=present, cold_segments={cold_ids_repr}"
5780 ));
5781 }
5782 s.push(')');
5783 s
5784 }
5785 _ => " (rows=—)".to_string(),
5786 };
5787 line.push_str(&annot);
5788 continue;
5789 }
5790 line.push_str(" (rows=—)");
5792 }
5793}
5794
5795fn explain_select(stmt: &SelectStatement, engine: &Engine, depth: usize, out: &mut Vec<String>) {
5796 let pad = " ".repeat(depth);
5797 let top = if !stmt.ctes.is_empty() {
5799 if stmt.ctes.iter().any(|c| c.recursive) {
5800 "CTEScan (WITH RECURSIVE)"
5801 } else {
5802 "CTEScan (WITH)"
5803 }
5804 } else if !stmt.unions.is_empty() {
5805 "UnionScan"
5806 } else if select_has_window(stmt) {
5807 "WindowAgg"
5808 } else if aggregate::uses_aggregate(stmt) {
5809 "Aggregate"
5810 } else if stmt.distinct {
5811 "Distinct"
5812 } else if stmt.from.is_some() {
5813 "TableScan"
5814 } else {
5815 "Result"
5816 };
5817 out.push(alloc::format!("{pad}{top}"));
5818 let child = " ".repeat(depth + 1);
5819 for cte in &stmt.ctes {
5821 let head = if cte.recursive {
5822 alloc::format!("{child}CTE (recursive): {}", cte.name)
5823 } else {
5824 alloc::format!("{child}CTE: {}", cte.name)
5825 };
5826 out.push(head);
5827 explain_select(&cte.body, engine, depth + 2, out);
5828 }
5829 if let Some(from) = &stmt.from {
5831 let mut tag = alloc::format!("{child}From: {}", from.primary.name);
5832 if let Some(alias) = &from.primary.alias {
5833 tag.push_str(&alloc::format!(" AS {alias}"));
5834 }
5835 if let Some(w) = &stmt.where_
5838 && let Some(table) = engine.active_catalog().get(&from.primary.name)
5839 {
5840 let alias = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
5841 let cols = &table.schema().columns;
5842 if try_index_seek(w, cols, engine.active_catalog(), table, alias).is_some() {
5843 tag.push_str(" [index seek]");
5844 } else {
5845 tag.push_str(" [full scan]");
5846 }
5847 } else {
5848 tag.push_str(" [full scan]");
5849 }
5850 out.push(tag);
5851 for j in &from.joins {
5852 let kind = match j.kind {
5853 spg_sql::ast::JoinKind::Inner => "INNER JOIN",
5854 spg_sql::ast::JoinKind::Left => "LEFT JOIN",
5855 spg_sql::ast::JoinKind::Cross => "CROSS JOIN",
5856 };
5857 let mut s = alloc::format!("{child}{kind}: {}", j.table.name);
5858 if let Some(alias) = &j.table.alias {
5859 s.push_str(&alloc::format!(" AS {alias}"));
5860 }
5861 if j.on.is_some() {
5862 s.push_str(" (ON …)");
5863 }
5864 out.push(s);
5865 }
5866 }
5867 if let Some(w) = &stmt.where_ {
5869 let mut s = alloc::format!("{child}Filter: {w}");
5870 if expr_has_subquery(w) {
5871 s.push_str(" [subquery]");
5872 }
5873 out.push(s);
5874 }
5875 if let Some(gs) = &stmt.group_by {
5876 let mut parts = Vec::new();
5877 for g in gs {
5878 parts.push(alloc::format!("{g}"));
5879 }
5880 out.push(alloc::format!("{child}GroupBy: {}", parts.join(", ")));
5881 }
5882 if let Some(h) = &stmt.having {
5883 out.push(alloc::format!("{child}Having: {h}"));
5884 }
5885 for o in &stmt.order_by {
5886 let dir = if o.desc { "DESC" } else { "ASC" };
5887 out.push(alloc::format!("{child}OrderBy: {} {dir}", o.expr));
5888 }
5889 if let Some(lim) = stmt.limit {
5890 out.push(alloc::format!("{child}Limit: {lim}"));
5891 }
5892 if let Some(off) = stmt.offset {
5893 out.push(alloc::format!("{child}Offset: {off}"));
5894 }
5895 if stmt
5897 .items
5898 .iter()
5899 .any(|it| matches!(it, SelectItem::Wildcard))
5900 {
5901 out.push(alloc::format!("{child}Project: *"));
5902 } else {
5903 out.push(alloc::format!(
5904 "{child}Project: {} item(s)",
5905 stmt.items.len()
5906 ));
5907 }
5908 for (kind, peer) in &stmt.unions {
5910 let label = match kind {
5911 UnionKind::All => "UNION ALL",
5912 UnionKind::Distinct => "UNION",
5913 };
5914 out.push(alloc::format!("{child}{label}"));
5915 explain_select(peer, engine, depth + 2, out);
5916 }
5917}
5918
5919fn is_correlation_error(e: &EngineError) -> bool {
5924 matches!(
5925 e,
5926 EngineError::Eval(
5927 eval::EvalError::ColumnNotFound { .. } | eval::EvalError::UnknownQualifier { .. }
5928 )
5929 )
5930}
5931
5932fn substitute_outer_columns(stmt: &mut SelectStatement, row: &Row, ctx: &EvalContext<'_>) {
5940 let Some(outer_alias) = ctx.table_alias else {
5941 return;
5942 };
5943 substitute_in_select(stmt, row, ctx, outer_alias);
5944}
5945
5946fn substitute_in_select(
5947 stmt: &mut SelectStatement,
5948 row: &Row,
5949 ctx: &EvalContext<'_>,
5950 outer_alias: &str,
5951) {
5952 for item in &mut stmt.items {
5953 if let SelectItem::Expr { expr, .. } = item {
5954 substitute_in_expr(expr, row, ctx, outer_alias);
5955 }
5956 }
5957 if let Some(w) = &mut stmt.where_ {
5958 substitute_in_expr(w, row, ctx, outer_alias);
5959 }
5960 if let Some(gs) = &mut stmt.group_by {
5961 for g in gs {
5962 substitute_in_expr(g, row, ctx, outer_alias);
5963 }
5964 }
5965 if let Some(h) = &mut stmt.having {
5966 substitute_in_expr(h, row, ctx, outer_alias);
5967 }
5968 for o in &mut stmt.order_by {
5969 substitute_in_expr(&mut o.expr, row, ctx, outer_alias);
5970 }
5971 for (_, peer) in &mut stmt.unions {
5972 substitute_in_select(peer, row, ctx, outer_alias);
5973 }
5974}
5975
5976fn substitute_in_expr(e: &mut Expr, row: &Row, ctx: &EvalContext<'_>, outer_alias: &str) {
5977 if let Expr::Column(c) = e
5978 && let Some(qual) = &c.qualifier
5979 && qual.eq_ignore_ascii_case(outer_alias)
5980 {
5981 if let Some(idx) = ctx
5983 .columns
5984 .iter()
5985 .position(|sc| sc.name.eq_ignore_ascii_case(&c.name))
5986 {
5987 let v = row.values.get(idx).cloned().unwrap_or(Value::Null);
5988 if let Ok(lit) = value_to_literal_expr(v) {
5989 *e = lit;
5990 return;
5991 }
5992 }
5993 }
5994 match e {
5995 Expr::Binary { lhs, rhs, .. } => {
5996 substitute_in_expr(lhs, row, ctx, outer_alias);
5997 substitute_in_expr(rhs, row, ctx, outer_alias);
5998 }
5999 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6000 substitute_in_expr(expr, row, ctx, outer_alias);
6001 }
6002 Expr::Like { expr, pattern, .. } => {
6003 substitute_in_expr(expr, row, ctx, outer_alias);
6004 substitute_in_expr(pattern, row, ctx, outer_alias);
6005 }
6006 Expr::FunctionCall { args, .. } => {
6007 for a in args {
6008 substitute_in_expr(a, row, ctx, outer_alias);
6009 }
6010 }
6011 Expr::Extract { source, .. } => substitute_in_expr(source, row, ctx, outer_alias),
6012 Expr::WindowFunction {
6013 args,
6014 partition_by,
6015 order_by,
6016 ..
6017 } => {
6018 for a in args {
6019 substitute_in_expr(a, row, ctx, outer_alias);
6020 }
6021 for p in partition_by {
6022 substitute_in_expr(p, row, ctx, outer_alias);
6023 }
6024 for (o, _) in order_by {
6025 substitute_in_expr(o, row, ctx, outer_alias);
6026 }
6027 }
6028 Expr::ScalarSubquery(s) => substitute_in_select(s, row, ctx, outer_alias),
6029 Expr::Exists { subquery, .. } | Expr::InSubquery { subquery, .. } => {
6030 substitute_in_select(subquery, row, ctx, outer_alias);
6031 }
6032 Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {}
6033 }
6034}
6035
6036fn encode_row_key(row: &Row) -> Vec<u8> {
6040 let mut out = Vec::new();
6041 for v in &row.values {
6042 let s = alloc::format!("{v:?}|");
6043 out.extend_from_slice(s.as_bytes());
6044 }
6045 out
6046}
6047
6048fn select_has_window(stmt: &SelectStatement) -> bool {
6049 for item in &stmt.items {
6050 if let SelectItem::Expr { expr, .. } = item
6051 && expr_has_window(expr)
6052 {
6053 return true;
6054 }
6055 }
6056 false
6057}
6058
6059fn expr_has_window(e: &Expr) -> bool {
6060 match e {
6061 Expr::WindowFunction { .. } => true,
6062 Expr::Binary { lhs, rhs, .. } => expr_has_window(lhs) || expr_has_window(rhs),
6063 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6064 expr_has_window(expr)
6065 }
6066 Expr::FunctionCall { args, .. } => args.iter().any(expr_has_window),
6067 Expr::Like { expr, pattern, .. } => expr_has_window(expr) || expr_has_window(pattern),
6068 Expr::Extract { source, .. } => expr_has_window(source),
6069 Expr::ScalarSubquery(_)
6070 | Expr::Exists { .. }
6071 | Expr::InSubquery { .. }
6072 | Expr::Literal(_)
6073 | Expr::Placeholder(_)
6074 | Expr::Column(_) => false,
6075 }
6076}
6077
6078fn collect_window_nodes(e: &Expr, out: &mut Vec<Expr>) {
6079 if let Expr::WindowFunction { .. } = e {
6080 if !out.iter().any(|x| x == e) {
6085 out.push(e.clone());
6086 }
6087 return;
6088 }
6089 match e {
6090 Expr::WindowFunction { .. } => unreachable!(),
6092 Expr::Binary { lhs, rhs, .. } => {
6093 collect_window_nodes(lhs, out);
6094 collect_window_nodes(rhs, out);
6095 }
6096 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6097 collect_window_nodes(expr, out);
6098 }
6099 Expr::FunctionCall { args, .. } => {
6100 for a in args {
6101 collect_window_nodes(a, out);
6102 }
6103 }
6104 Expr::Like { expr, pattern, .. } => {
6105 collect_window_nodes(expr, out);
6106 collect_window_nodes(pattern, out);
6107 }
6108 Expr::Extract { source, .. } => collect_window_nodes(source, out),
6109 _ => {}
6110 }
6111}
6112
6113fn rewrite_window_to_columns(e: &mut Expr, window_nodes: &[Expr]) {
6114 if let Expr::WindowFunction { .. } = e
6115 && let Some(idx) = window_nodes.iter().position(|w| w == e)
6116 {
6117 *e = Expr::Column(spg_sql::ast::ColumnName {
6118 qualifier: None,
6119 name: alloc::format!("__win_{idx}"),
6120 });
6121 return;
6122 }
6123 match e {
6124 Expr::Binary { lhs, rhs, .. } => {
6125 rewrite_window_to_columns(lhs, window_nodes);
6126 rewrite_window_to_columns(rhs, window_nodes);
6127 }
6128 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6129 rewrite_window_to_columns(expr, window_nodes);
6130 }
6131 Expr::FunctionCall { args, .. } => {
6132 for a in args {
6133 rewrite_window_to_columns(a, window_nodes);
6134 }
6135 }
6136 Expr::Like { expr, pattern, .. } => {
6137 rewrite_window_to_columns(expr, window_nodes);
6138 rewrite_window_to_columns(pattern, window_nodes);
6139 }
6140 Expr::Extract { source, .. } => rewrite_window_to_columns(source, window_nodes),
6141 _ => {}
6142 }
6143}
6144
6145fn partition_key_cmp(a: &[Value], b: &[Value]) -> core::cmp::Ordering {
6149 for (x, y) in a.iter().zip(b.iter()) {
6150 let c = value_cmp(x, y);
6151 if c != core::cmp::Ordering::Equal {
6152 return c;
6153 }
6154 }
6155 a.len().cmp(&b.len())
6156}
6157
6158fn order_key_cmp(a: &[(Value, bool)], b: &[(Value, bool)]) -> core::cmp::Ordering {
6159 for ((va, desc), (vb, _)) in a.iter().zip(b.iter()) {
6160 let c = value_cmp(va, vb);
6161 let c = if *desc { c.reverse() } else { c };
6162 if c != core::cmp::Ordering::Equal {
6163 return c;
6164 }
6165 }
6166 a.len().cmp(&b.len())
6167}
6168
6169#[allow(clippy::match_same_arms)] fn value_cmp(a: &Value, b: &Value) -> core::cmp::Ordering {
6171 use core::cmp::Ordering;
6172 match (a, b) {
6173 (Value::Null, Value::Null) => Ordering::Equal,
6174 (Value::Null, _) => Ordering::Less,
6175 (_, Value::Null) => Ordering::Greater,
6176 (Value::Int(x), Value::Int(y)) => x.cmp(y),
6177 (Value::BigInt(x), Value::BigInt(y)) => x.cmp(y),
6178 (Value::SmallInt(x), Value::SmallInt(y)) => x.cmp(y),
6179 (Value::Text(x), Value::Text(y)) => x.cmp(y),
6180 (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
6181 (Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
6182 (Value::Date(x), Value::Date(y)) => x.cmp(y),
6183 (Value::Timestamp(x), Value::Timestamp(y)) => x.cmp(y),
6184 _ => alloc::format!("{a:?}").cmp(&alloc::format!("{b:?}")),
6187 }
6188}
6189
6190#[allow(
6196 clippy::too_many_arguments,
6197 clippy::cast_possible_truncation,
6198 clippy::cast_possible_wrap,
6199 clippy::cast_precision_loss,
6200 clippy::cast_sign_loss,
6201 clippy::doc_markdown,
6202 clippy::too_many_lines,
6203 clippy::type_complexity,
6204 clippy::match_same_arms
6205)]
6206fn compute_window_partition(
6207 name: &str,
6208 args: &[Expr],
6209 ordered: bool,
6210 frame: Option<&WindowFrame>,
6211 null_treatment: spg_sql::ast::NullTreatment,
6212 slice: &[(Vec<Value>, Vec<(Value, bool)>, usize)],
6213 filtered_rows: &[&Row],
6214 ctx: &EvalContext<'_>,
6215 out_vals: &mut [Value],
6216) -> Result<(), EngineError> {
6217 let ignore_nulls = matches!(null_treatment, spg_sql::ast::NullTreatment::Ignore);
6218 let lower = name.to_ascii_lowercase();
6219 match lower.as_str() {
6220 "row_number" => {
6221 for (rank, (_, _, idx)) in slice.iter().enumerate() {
6222 out_vals[*idx] = Value::BigInt((rank + 1) as i64);
6223 }
6224 Ok(())
6225 }
6226 "rank" => {
6227 let mut prev_key: Option<&[(Value, bool)]> = None;
6228 let mut current_rank: i64 = 1;
6229 for (i, (_, okey, idx)) in slice.iter().enumerate() {
6230 if let Some(p) = prev_key
6231 && order_key_cmp(p, okey) != core::cmp::Ordering::Equal
6232 {
6233 current_rank = (i + 1) as i64;
6234 }
6235 if prev_key.is_none() {
6236 current_rank = 1;
6237 }
6238 out_vals[*idx] = Value::BigInt(current_rank);
6239 prev_key = Some(okey.as_slice());
6240 }
6241 Ok(())
6242 }
6243 "dense_rank" => {
6244 let mut prev_key: Option<&[(Value, bool)]> = None;
6245 let mut current_rank: i64 = 0;
6246 for (_, okey, idx) in slice {
6247 if prev_key.is_none_or(|p| order_key_cmp(p, okey) != core::cmp::Ordering::Equal) {
6248 current_rank += 1;
6249 }
6250 out_vals[*idx] = Value::BigInt(current_rank);
6251 prev_key = Some(okey.as_slice());
6252 }
6253 Ok(())
6254 }
6255 "sum" | "avg" | "min" | "max" | "count" | "count_star" => {
6256 let arg_values: Vec<Value> = if lower == "count_star" || args.is_empty() {
6259 slice.iter().map(|_| Value::Null).collect()
6260 } else {
6261 slice
6262 .iter()
6263 .map(|(_, _, idx)| eval::eval_expr(&args[0], filtered_rows[*idx], ctx))
6264 .collect::<Result<_, _>>()
6265 .map_err(EngineError::Eval)?
6266 };
6267 let eff = effective_frame(frame, ordered)?;
6271 #[allow(clippy::needless_range_loop)]
6272 for i in 0..slice.len() {
6273 let (lo, hi) = frame_bounds_for_row(&eff, i, slice);
6274 let mut sum: f64 = 0.0;
6275 let mut count: i64 = 0;
6276 let mut min_v: Option<f64> = None;
6277 let mut max_v: Option<f64> = None;
6278 let mut row_count: i64 = 0;
6279 if lo <= hi {
6280 for j in lo..=hi {
6281 let v = &arg_values[j];
6282 match lower.as_str() {
6283 "count_star" => row_count += 1,
6284 "count" => {
6285 if !v.is_null() {
6286 count += 1;
6287 }
6288 }
6289 _ => {
6290 if let Some(x) = value_to_f64(v) {
6291 sum += x;
6292 count += 1;
6293 min_v = Some(min_v.map_or(x, |m| m.min(x)));
6294 max_v = Some(max_v.map_or(x, |m| m.max(x)));
6295 }
6296 }
6297 }
6298 }
6299 }
6300 let value = match lower.as_str() {
6301 "count_star" => Value::BigInt(row_count),
6302 "count" => Value::BigInt(count),
6303 "sum" => Value::Float(sum),
6304 "avg" => {
6305 if count == 0 {
6306 Value::Null
6307 } else {
6308 Value::Float(sum / count as f64)
6309 }
6310 }
6311 "min" => min_v.map_or(Value::Null, Value::Float),
6312 "max" => max_v.map_or(Value::Null, Value::Float),
6313 _ => unreachable!(),
6314 };
6315 let (_, _, idx) = &slice[i];
6316 out_vals[*idx] = value;
6317 }
6318 Ok(())
6319 }
6320 "lag" | "lead" => {
6321 if args.is_empty() {
6324 return Err(EngineError::Unsupported(alloc::format!(
6325 "{lower}() requires at least one argument"
6326 )));
6327 }
6328 let offset: i64 = if args.len() >= 2 {
6329 let v = eval::eval_expr(&args[1], filtered_rows[slice[0].2], ctx)
6330 .map_err(EngineError::Eval)?;
6331 match v {
6332 Value::SmallInt(n) => i64::from(n),
6333 Value::Int(n) => i64::from(n),
6334 Value::BigInt(n) => n,
6335 _ => {
6336 return Err(EngineError::Unsupported(alloc::format!(
6337 "{lower}() offset must be integer"
6338 )));
6339 }
6340 }
6341 } else {
6342 1
6343 };
6344 let default: Value = if args.len() >= 3 {
6345 eval::eval_expr(&args[2], filtered_rows[slice[0].2], ctx)
6346 .map_err(EngineError::Eval)?
6347 } else {
6348 Value::Null
6349 };
6350 let values: Vec<Value> = slice
6351 .iter()
6352 .map(|(_, _, idx)| eval::eval_expr(&args[0], filtered_rows[*idx], ctx))
6353 .collect::<Result<_, _>>()
6354 .map_err(EngineError::Eval)?;
6355 let n = slice.len();
6356 for (i, (_, _, idx)) in slice.iter().enumerate() {
6357 let signed_offset = if lower == "lag" { -offset } else { offset };
6358 let v = if ignore_nulls {
6359 let step: i64 = if signed_offset >= 0 { 1 } else { -1 };
6363 let needed: i64 = signed_offset.abs();
6364 if needed == 0 {
6365 values[i].clone()
6366 } else {
6367 let mut j: i64 = i as i64;
6368 let mut hits: i64 = 0;
6369 let mut found: Option<Value> = None;
6370 loop {
6371 j += step;
6372 if j < 0 || j >= n as i64 {
6373 break;
6374 }
6375 #[allow(clippy::cast_sign_loss)]
6376 let v = &values[j as usize];
6377 if !v.is_null() {
6378 hits += 1;
6379 if hits == needed {
6380 found = Some(v.clone());
6381 break;
6382 }
6383 }
6384 }
6385 found.unwrap_or_else(|| default.clone())
6386 }
6387 } else {
6388 let target_signed = i64::try_from(i).unwrap_or(i64::MAX) + signed_offset;
6389 if target_signed < 0
6390 || target_signed >= i64::try_from(n).unwrap_or(i64::MAX)
6391 {
6392 default.clone()
6393 } else {
6394 #[allow(clippy::cast_sign_loss)]
6395 {
6396 values[target_signed as usize].clone()
6397 }
6398 }
6399 };
6400 out_vals[*idx] = v;
6401 }
6402 Ok(())
6403 }
6404 "first_value" | "last_value" | "nth_value" => {
6405 if args.is_empty() {
6406 return Err(EngineError::Unsupported(alloc::format!(
6407 "{lower}() requires at least one argument"
6408 )));
6409 }
6410 let values: Vec<Value> = slice
6411 .iter()
6412 .map(|(_, _, idx)| eval::eval_expr(&args[0], filtered_rows[*idx], ctx))
6413 .collect::<Result<_, _>>()
6414 .map_err(EngineError::Eval)?;
6415 let nth: usize = if lower == "nth_value" {
6416 if args.len() < 2 {
6417 return Err(EngineError::Unsupported(
6418 "nth_value() requires (expr, n)".into(),
6419 ));
6420 }
6421 let v = eval::eval_expr(&args[1], filtered_rows[slice[0].2], ctx)
6422 .map_err(EngineError::Eval)?;
6423 let raw = match v {
6424 Value::SmallInt(n) => i64::from(n),
6425 Value::Int(n) => i64::from(n),
6426 Value::BigInt(n) => n,
6427 _ => {
6428 return Err(EngineError::Unsupported(
6429 "nth_value() n must be integer".into(),
6430 ));
6431 }
6432 };
6433 if raw < 1 {
6434 return Err(EngineError::Unsupported(
6435 "nth_value() n must be >= 1".into(),
6436 ));
6437 }
6438 #[allow(clippy::cast_sign_loss)]
6439 {
6440 raw as usize
6441 }
6442 } else {
6443 0
6444 };
6445 let eff = effective_frame(frame, ordered)?;
6446 for i in 0..slice.len() {
6447 let (lo, hi) = frame_bounds_for_row(&eff, i, slice);
6448 let (_, _, idx) = &slice[i];
6449 let v = if lo > hi {
6450 Value::Null
6451 } else if ignore_nulls && matches!(lower.as_str(), "first_value" | "last_value") {
6452 if lower == "first_value" {
6455 (lo..=hi)
6456 .find_map(|j| {
6457 let v = &values[j];
6458 (!v.is_null()).then(|| v.clone())
6459 })
6460 .unwrap_or(Value::Null)
6461 } else {
6462 (lo..=hi)
6463 .rev()
6464 .find_map(|j| {
6465 let v = &values[j];
6466 (!v.is_null()).then(|| v.clone())
6467 })
6468 .unwrap_or(Value::Null)
6469 }
6470 } else {
6471 match lower.as_str() {
6472 "first_value" => values[lo].clone(),
6473 "last_value" => values[hi].clone(),
6474 "nth_value" => {
6475 let pos = lo + nth - 1;
6476 if pos > hi {
6477 Value::Null
6478 } else {
6479 values[pos].clone()
6480 }
6481 }
6482 _ => unreachable!(),
6483 }
6484 };
6485 out_vals[*idx] = v;
6486 }
6487 Ok(())
6488 }
6489 "ntile" => {
6490 if args.is_empty() {
6491 return Err(EngineError::Unsupported(
6492 "ntile(n) requires an integer argument".into(),
6493 ));
6494 }
6495 let v = eval::eval_expr(&args[0], filtered_rows[slice[0].2], ctx)
6496 .map_err(EngineError::Eval)?;
6497 let bucket_count: i64 = match v {
6498 Value::SmallInt(n) => i64::from(n),
6499 Value::Int(n) => i64::from(n),
6500 Value::BigInt(n) => n,
6501 _ => {
6502 return Err(EngineError::Unsupported(
6503 "ntile() argument must be integer".into(),
6504 ));
6505 }
6506 };
6507 if bucket_count < 1 {
6508 return Err(EngineError::Unsupported(
6509 "ntile() argument must be >= 1".into(),
6510 ));
6511 }
6512 #[allow(clippy::cast_sign_loss)]
6513 let buckets = bucket_count as usize;
6514 let n = slice.len();
6515 let base = n / buckets;
6518 let extras = n % buckets;
6519 let mut bucket: usize = 1;
6520 let mut remaining_in_bucket = if extras > 0 { base + 1 } else { base };
6521 let mut buckets_with_extra_remaining = extras;
6522 for (_, _, idx) in slice {
6523 if remaining_in_bucket == 0 {
6524 bucket += 1;
6525 buckets_with_extra_remaining = buckets_with_extra_remaining.saturating_sub(1);
6526 remaining_in_bucket = if buckets_with_extra_remaining > 0 {
6527 base + 1
6528 } else {
6529 base
6530 };
6531 if remaining_in_bucket == 0 {
6534 remaining_in_bucket = 1;
6535 }
6536 }
6537 out_vals[*idx] = Value::BigInt(i64::try_from(bucket).unwrap_or(i64::MAX));
6538 remaining_in_bucket -= 1;
6539 }
6540 Ok(())
6541 }
6542 "percent_rank" => {
6543 let n = slice.len();
6546 let mut prev_key: Option<&[(Value, bool)]> = None;
6547 let mut current_rank: i64 = 1;
6548 for (i, (_, okey, idx)) in slice.iter().enumerate() {
6549 if let Some(p) = prev_key
6550 && order_key_cmp(p, okey) != core::cmp::Ordering::Equal
6551 {
6552 current_rank = i64::try_from(i + 1).unwrap_or(i64::MAX);
6553 }
6554 if prev_key.is_none() {
6555 current_rank = 1;
6556 }
6557 #[allow(clippy::cast_precision_loss)]
6558 let pr = if n <= 1 {
6559 0.0
6560 } else {
6561 (current_rank - 1) as f64 / (n - 1) as f64
6562 };
6563 out_vals[*idx] = Value::Float(pr);
6564 prev_key = Some(okey.as_slice());
6565 }
6566 Ok(())
6567 }
6568 "cume_dist" => {
6569 let n = slice.len();
6571 for i in 0..slice.len() {
6573 let peer_end = peer_group_end(slice, i);
6574 #[allow(clippy::cast_precision_loss)]
6575 let cd = (peer_end + 1) as f64 / n as f64;
6576 let (_, _, idx) = &slice[i];
6577 out_vals[*idx] = Value::Float(cd);
6578 }
6579 Ok(())
6580 }
6581 other => Err(EngineError::Unsupported(alloc::format!(
6582 "window function {other:?} not supported (v4.21: row_number/rank/dense_rank/sum/avg/count/min/max/lag/lead/first_value/last_value/nth_value/ntile/percent_rank/cume_dist)"
6583 ))),
6584 }
6585}
6586
6587fn effective_frame(
6594 frame: Option<&WindowFrame>,
6595 ordered: bool,
6596) -> Result<(FrameKind, FrameBound, FrameBound), EngineError> {
6597 match frame {
6598 None => {
6599 if ordered {
6600 Ok((
6601 FrameKind::Range,
6602 FrameBound::UnboundedPreceding,
6603 FrameBound::CurrentRow,
6604 ))
6605 } else {
6606 Ok((
6607 FrameKind::Rows,
6608 FrameBound::UnboundedPreceding,
6609 FrameBound::UnboundedFollowing,
6610 ))
6611 }
6612 }
6613 Some(fr) => {
6614 let end = fr.end.clone().unwrap_or(FrameBound::CurrentRow);
6615 if matches!(fr.start, FrameBound::UnboundedFollowing)
6617 || matches!(end, FrameBound::UnboundedPreceding)
6618 {
6619 return Err(EngineError::Unsupported(alloc::format!(
6620 "invalid frame: start={:?} end={:?}",
6621 fr.start,
6622 end
6623 )));
6624 }
6625 if fr.kind == FrameKind::Range
6630 && (matches!(
6631 fr.start,
6632 FrameBound::OffsetPreceding(_) | FrameBound::OffsetFollowing(_)
6633 ) || matches!(
6634 end,
6635 FrameBound::OffsetPreceding(_) | FrameBound::OffsetFollowing(_)
6636 ))
6637 {
6638 return Err(EngineError::Unsupported(
6639 "RANGE with explicit offset bounds is not supported (v4.20: only UNBOUNDED / CURRENT ROW for RANGE)".into(),
6640 ));
6641 }
6642 Ok((fr.kind, fr.start.clone(), end))
6643 }
6644 }
6645}
6646
6647#[allow(clippy::type_complexity)]
6651fn frame_bounds_for_row(
6652 eff: &(FrameKind, FrameBound, FrameBound),
6653 i: usize,
6654 slice: &[(Vec<Value>, Vec<(Value, bool)>, usize)],
6655) -> (usize, usize) {
6656 let (kind, start, end) = eff;
6657 let n = slice.len();
6658 let last = n.saturating_sub(1);
6659 let (mut lo, mut hi) = match kind {
6660 FrameKind::Rows => {
6661 let lo = match start {
6662 FrameBound::UnboundedPreceding => 0,
6663 FrameBound::OffsetPreceding(k) => {
6664 let k = usize::try_from(*k).unwrap_or(usize::MAX);
6665 i.saturating_sub(k)
6666 }
6667 FrameBound::CurrentRow => i,
6668 FrameBound::OffsetFollowing(k) => {
6669 let k = usize::try_from(*k).unwrap_or(usize::MAX);
6670 i.saturating_add(k).min(last)
6671 }
6672 FrameBound::UnboundedFollowing => last,
6673 };
6674 let hi = match end {
6675 FrameBound::UnboundedPreceding => 0,
6676 FrameBound::OffsetPreceding(k) => {
6677 let k = usize::try_from(*k).unwrap_or(usize::MAX);
6678 i.saturating_sub(k)
6679 }
6680 FrameBound::CurrentRow => i,
6681 FrameBound::OffsetFollowing(k) => {
6682 let k = usize::try_from(*k).unwrap_or(usize::MAX);
6683 i.saturating_add(k).min(last)
6684 }
6685 FrameBound::UnboundedFollowing => last,
6686 };
6687 (lo, hi)
6688 }
6689 FrameKind::Range => {
6690 let lo = match start {
6696 FrameBound::UnboundedPreceding => 0,
6697 FrameBound::CurrentRow => peer_group_start(slice, i),
6698 FrameBound::UnboundedFollowing => last,
6699 _ => unreachable!("offset bounds rejected for RANGE"),
6700 };
6701 let hi = match end {
6702 FrameBound::UnboundedPreceding => 0,
6703 FrameBound::CurrentRow => peer_group_end(slice, i),
6704 FrameBound::UnboundedFollowing => last,
6705 _ => unreachable!("offset bounds rejected for RANGE"),
6706 };
6707 (lo, hi)
6708 }
6709 };
6710 if hi >= n {
6711 hi = last;
6712 }
6713 if lo >= n {
6714 lo = last;
6715 }
6716 (lo, hi)
6717}
6718
6719#[allow(clippy::type_complexity)]
6723fn peer_group_start(slice: &[(Vec<Value>, Vec<(Value, bool)>, usize)], i: usize) -> usize {
6724 let key = &slice[i].1;
6725 let mut j = i;
6726 while j > 0 && order_key_cmp(&slice[j - 1].1, key) == core::cmp::Ordering::Equal {
6727 j -= 1;
6728 }
6729 j
6730}
6731
6732#[allow(clippy::type_complexity)]
6735fn peer_group_end(slice: &[(Vec<Value>, Vec<(Value, bool)>, usize)], i: usize) -> usize {
6736 let key = &slice[i].1;
6737 let mut j = i;
6738 while j + 1 < slice.len() && order_key_cmp(&slice[j + 1].1, key) == core::cmp::Ordering::Equal {
6739 j += 1;
6740 }
6741 j
6742}
6743
6744fn value_to_f64(v: &Value) -> Option<f64> {
6745 match v {
6746 Value::SmallInt(n) => Some(f64::from(*n)),
6747 Value::Int(n) => Some(f64::from(*n)),
6748 #[allow(clippy::cast_precision_loss)]
6749 Value::BigInt(n) => Some(*n as f64),
6750 Value::Float(x) => Some(*x),
6751 _ => None,
6752 }
6753}
6754
6755fn expr_tree_has_subquery(stmt: &SelectStatement) -> bool {
6759 let mut any = false;
6760 for item in &stmt.items {
6761 if let SelectItem::Expr { expr, .. } = item {
6762 any = any || expr_has_subquery(expr);
6763 }
6764 }
6765 if let Some(w) = &stmt.where_ {
6766 any = any || expr_has_subquery(w);
6767 }
6768 if let Some(h) = &stmt.having {
6769 any = any || expr_has_subquery(h);
6770 }
6771 for o in &stmt.order_by {
6772 any = any || expr_has_subquery(&o.expr);
6773 }
6774 for (_, peer) in &stmt.unions {
6775 any = any || expr_tree_has_subquery(peer);
6776 }
6777 any
6778}
6779
6780fn expr_has_subquery(e: &Expr) -> bool {
6781 match e {
6782 Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => true,
6783 Expr::Binary { lhs, rhs, .. } => expr_has_subquery(lhs) || expr_has_subquery(rhs),
6784 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6785 expr_has_subquery(expr)
6786 }
6787 Expr::FunctionCall { args, .. } => args.iter().any(expr_has_subquery),
6788 Expr::Like { expr, pattern, .. } => expr_has_subquery(expr) || expr_has_subquery(pattern),
6789 Expr::Extract { source, .. } => expr_has_subquery(source),
6790 Expr::WindowFunction {
6791 args,
6792 partition_by,
6793 order_by,
6794 ..
6795 } => {
6796 args.iter().any(expr_has_subquery)
6797 || partition_by.iter().any(expr_has_subquery)
6798 || order_by.iter().any(|(e, _)| expr_has_subquery(e))
6799 }
6800 Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => false,
6801 }
6802}
6803
6804fn value_to_literal_expr(v: Value) -> Result<Expr, EngineError> {
6811 let lit = match v {
6812 Value::Null => Literal::Null,
6813 Value::SmallInt(n) => Literal::Integer(i64::from(n)),
6814 Value::Int(n) => Literal::Integer(i64::from(n)),
6815 Value::BigInt(n) => Literal::Integer(n),
6816 Value::Float(x) => Literal::Float(x),
6817 Value::Text(s) | Value::Json(s) => Literal::String(s),
6818 Value::Bool(b) => Literal::Bool(b),
6819 other => {
6820 return Err(EngineError::Unsupported(alloc::format!(
6821 "subquery result type {:?} not yet materialisable; cast to text or integer in the inner SELECT",
6822 other.data_type()
6823 )));
6824 }
6825 };
6826 Ok(Expr::Literal(lit))
6827}
6828
6829fn substitute_placeholders(stmt: &mut Statement, params: &[Value]) -> Result<(), EngineError> {
6840 match stmt {
6841 Statement::Select(s) => substitute_select(s, params)?,
6842 Statement::Insert(ins) => {
6843 for row in &mut ins.rows {
6844 for e in row {
6845 substitute_expr(e, params)?;
6846 }
6847 }
6848 }
6849 Statement::Update(u) => {
6850 for (_, e) in &mut u.assignments {
6851 substitute_expr(e, params)?;
6852 }
6853 if let Some(w) = &mut u.where_ {
6854 substitute_expr(w, params)?;
6855 }
6856 }
6857 Statement::Delete(d) => {
6858 if let Some(w) = &mut d.where_ {
6859 substitute_expr(w, params)?;
6860 }
6861 }
6862 Statement::Explain(e) => substitute_select(&mut e.inner, params)?,
6863 _ => {}
6866 }
6867 Ok(())
6868}
6869
6870fn substitute_select(
6871 s: &mut SelectStatement,
6872 params: &[Value],
6873) -> Result<(), EngineError> {
6874 for item in &mut s.items {
6875 if let SelectItem::Expr { expr, .. } = item {
6876 substitute_expr(expr, params)?;
6877 }
6878 }
6879 if let Some(w) = &mut s.where_ {
6880 substitute_expr(w, params)?;
6881 }
6882 if let Some(gs) = &mut s.group_by {
6883 for g in gs {
6884 substitute_expr(g, params)?;
6885 }
6886 }
6887 if let Some(h) = &mut s.having {
6888 substitute_expr(h, params)?;
6889 }
6890 for o in &mut s.order_by {
6891 substitute_expr(&mut o.expr, params)?;
6892 }
6893 for (_, peer) in &mut s.unions {
6894 substitute_select(peer, params)?;
6895 }
6896 Ok(())
6897}
6898
6899fn substitute_expr(e: &mut Expr, params: &[Value]) -> Result<(), EngineError> {
6900 if let Expr::Placeholder(n) = e {
6901 let idx = usize::from(*n).saturating_sub(1);
6902 let v = params.get(idx).ok_or_else(|| {
6903 EngineError::Eval(EvalError::PlaceholderOutOfRange {
6904 n: *n,
6905 bound: u16::try_from(params.len()).unwrap_or(u16::MAX),
6906 })
6907 })?;
6908 *e = Expr::Literal(value_to_literal(v.clone()));
6909 return Ok(());
6910 }
6911 match e {
6912 Expr::Binary { lhs, rhs, .. } => {
6913 substitute_expr(lhs, params)?;
6914 substitute_expr(rhs, params)?;
6915 }
6916 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6917 substitute_expr(expr, params)?;
6918 }
6919 Expr::FunctionCall { args, .. } => {
6920 for a in args {
6921 substitute_expr(a, params)?;
6922 }
6923 }
6924 Expr::Like { expr, pattern, .. } => {
6925 substitute_expr(expr, params)?;
6926 substitute_expr(pattern, params)?;
6927 }
6928 Expr::Extract { source, .. } => substitute_expr(source, params)?,
6929 Expr::ScalarSubquery(s) => substitute_select(s, params)?,
6930 Expr::Exists { subquery, .. } => substitute_select(subquery, params)?,
6931 Expr::InSubquery { expr, subquery, .. } => {
6932 substitute_expr(expr, params)?;
6933 substitute_select(subquery, params)?;
6934 }
6935 Expr::WindowFunction {
6936 args,
6937 partition_by,
6938 order_by,
6939 ..
6940 } => {
6941 for a in args {
6942 substitute_expr(a, params)?;
6943 }
6944 for p in partition_by {
6945 substitute_expr(p, params)?;
6946 }
6947 for (e, _) in order_by {
6948 substitute_expr(e, params)?;
6949 }
6950 }
6951 Expr::Literal(_) | Expr::Column(_) => {}
6952 Expr::Placeholder(_) => unreachable!("Placeholder handled at top of fn"),
6954 }
6955 Ok(())
6956}
6957
6958fn sort_values_for_histogram(a: &Value, b: &Value) -> core::cmp::Ordering {
6976 use core::cmp::Ordering;
6977 match (a, b) {
6978 (Value::SmallInt(a), Value::SmallInt(b)) => a.cmp(b),
6979 (Value::Int(a), Value::Int(b)) => a.cmp(b),
6980 (Value::BigInt(a), Value::BigInt(b)) => a.cmp(b),
6981 (Value::SmallInt(a), Value::Int(b)) => i32::from(*a).cmp(b),
6982 (Value::Int(a), Value::SmallInt(b)) => a.cmp(&i32::from(*b)),
6983 (Value::Int(a), Value::BigInt(b)) => i64::from(*a).cmp(b),
6984 (Value::BigInt(a), Value::Int(b)) => a.cmp(&i64::from(*b)),
6985 (Value::SmallInt(a), Value::BigInt(b)) => i64::from(*a).cmp(b),
6986 (Value::BigInt(a), Value::SmallInt(b)) => a.cmp(&i64::from(*b)),
6987 (Value::Float(a), Value::Float(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
6988 (Value::Text(a), Value::Text(b)) | (Value::Json(a), Value::Json(b)) => a.cmp(b),
6989 (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
6990 (Value::Date(a), Value::Date(b)) => a.cmp(b),
6991 (Value::Timestamp(a), Value::Timestamp(b)) => a.cmp(b),
6992 (Value::SmallInt(n), Value::Float(x)) => {
6994 (f64::from(*n)).partial_cmp(x).unwrap_or(Ordering::Equal)
6995 }
6996 (Value::Float(x), Value::SmallInt(n)) => {
6997 x.partial_cmp(&f64::from(*n)).unwrap_or(Ordering::Equal)
6998 }
6999 (Value::Int(n), Value::Float(x)) => {
7000 (f64::from(*n)).partial_cmp(x).unwrap_or(Ordering::Equal)
7001 }
7002 (Value::Float(x), Value::Int(n)) => {
7003 x.partial_cmp(&f64::from(*n)).unwrap_or(Ordering::Equal)
7004 }
7005 (Value::BigInt(n), Value::Float(x)) => {
7006 #[allow(clippy::cast_precision_loss)]
7007 let nf = *n as f64;
7008 nf.partial_cmp(x).unwrap_or(Ordering::Equal)
7009 }
7010 (Value::Float(x), Value::BigInt(n)) => {
7011 #[allow(clippy::cast_precision_loss)]
7012 let nf = *n as f64;
7013 x.partial_cmp(&nf).unwrap_or(Ordering::Equal)
7014 }
7015 _ => canonical_value_repr(a).cmp(&canonical_value_repr(b)),
7018 }
7019}
7020
7021fn render_histogram_bounds(bounds: &[alloc::string::String]) -> alloc::string::String {
7028 let mut out = alloc::string::String::with_capacity(bounds.len() * 8 + 2);
7029 out.push('[');
7030 for (i, b) in bounds.iter().enumerate() {
7031 if i > 0 {
7032 out.push_str(", ");
7033 }
7034 let needs_quote = b.contains([',', '[', ']', '"']) || b.is_empty();
7035 if needs_quote {
7036 out.push('"');
7037 for ch in b.chars() {
7038 if ch == '"' || ch == '\\' {
7039 out.push('\\');
7040 }
7041 out.push(ch);
7042 }
7043 out.push('"');
7044 } else {
7045 out.push_str(b);
7046 }
7047 }
7048 out.push(']');
7049 out
7050}
7051
7052pub(crate) fn canonical_value_repr(v: &Value) -> alloc::string::String {
7062 match v {
7063 Value::Null => "NULL".to_string(),
7064 Value::SmallInt(n) => alloc::format!("{n}"),
7065 Value::Int(n) => alloc::format!("{n}"),
7066 Value::BigInt(n) => alloc::format!("{n}"),
7067 Value::Float(x) => alloc::format!("{x:?}"),
7068 Value::Text(s) | Value::Json(s) => s.clone(),
7069 Value::Bool(b) => if *b { "t" } else { "f" }.to_string(),
7070 Value::Date(d) => eval::format_date(*d),
7071 Value::Timestamp(t) => eval::format_timestamp(*t),
7072 Value::Interval { months, micros } => eval::format_interval(*months, *micros),
7073 Value::Numeric { scaled, scale } => eval::format_numeric(*scaled, *scale),
7074 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
7075 alloc::format!("{v:?}")
7079 }
7080 _ => alloc::format!("{v:?}"),
7084 }
7085}
7086
7087const fn is_internal_table_name(_name: &str) -> bool {
7094 false
7095}
7096
7097fn value_to_literal(v: Value) -> Literal {
7098 match v {
7099 Value::Null => Literal::Null,
7100 Value::SmallInt(n) => Literal::Integer(i64::from(n)),
7101 Value::Int(n) => Literal::Integer(i64::from(n)),
7102 Value::BigInt(n) => Literal::Integer(n),
7103 Value::Float(x) => Literal::Float(x),
7104 Value::Text(s) | Value::Json(s) => Literal::String(s),
7105 Value::Bool(b) => Literal::Bool(b),
7106 Value::Vector(v) => Literal::Vector(v),
7107 Value::Numeric { scaled, scale } => {
7108 Literal::String(eval::format_numeric(scaled, scale))
7109 }
7110 Value::Date(d) => Literal::String(eval::format_date(d)),
7111 Value::Timestamp(t) => Literal::String(eval::format_timestamp(t)),
7112 Value::Interval { months, micros } => Literal::Interval {
7113 months,
7114 micros,
7115 text: eval::format_interval(months, micros),
7116 },
7117 Value::Sq8Vector(q) => Literal::Vector(spg_storage::quantize::dequantize(&q)),
7120 Value::HalfVector(h) => Literal::Vector(h.to_f32_vec()),
7121 v => Literal::String(alloc::format!("{v:?}")),
7125 }
7126}
7127
7128fn rewrite_clock_calls(stmt: &mut Statement, now_micros: Option<i64>) {
7129 let Some(now) = now_micros else {
7130 return;
7131 };
7132 match stmt {
7133 Statement::Select(s) => rewrite_select_clock(s, now),
7134 Statement::Insert(ins) => {
7135 for row in &mut ins.rows {
7136 for e in row {
7137 rewrite_expr_clock(e, now);
7138 }
7139 }
7140 }
7141 _ => {}
7142 }
7143}
7144
7145fn rewrite_select_clock(s: &mut SelectStatement, now: i64) {
7146 for item in &mut s.items {
7147 if let SelectItem::Expr { expr, .. } = item {
7148 rewrite_expr_clock(expr, now);
7149 }
7150 }
7151 if let Some(w) = &mut s.where_ {
7152 rewrite_expr_clock(w, now);
7153 }
7154 if let Some(gs) = &mut s.group_by {
7155 for g in gs {
7156 rewrite_expr_clock(g, now);
7157 }
7158 }
7159 if let Some(h) = &mut s.having {
7160 rewrite_expr_clock(h, now);
7161 }
7162 for o in &mut s.order_by {
7163 rewrite_expr_clock(&mut o.expr, now);
7164 }
7165 for (_, peer) in &mut s.unions {
7166 rewrite_select_clock(peer, now);
7167 }
7168}
7169
7170fn rewrite_expr_clock(e: &mut Expr, now: i64) {
7178 if let Some(replacement) = clock_replacement_for(e, now) {
7182 *e = replacement;
7183 return;
7184 }
7185 match e {
7186 Expr::Binary { lhs, rhs, .. } => {
7187 rewrite_expr_clock(lhs, now);
7188 rewrite_expr_clock(rhs, now);
7189 }
7190 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
7191 rewrite_expr_clock(expr, now);
7192 }
7193 Expr::FunctionCall { args, .. } => {
7194 for a in args {
7195 rewrite_expr_clock(a, now);
7196 }
7197 }
7198 Expr::Like { expr, pattern, .. } => {
7199 rewrite_expr_clock(expr, now);
7200 rewrite_expr_clock(pattern, now);
7201 }
7202 Expr::Extract { source, .. } => rewrite_expr_clock(source, now),
7203 Expr::ScalarSubquery(s) => rewrite_select_clock(s, now),
7207 Expr::Exists { subquery, .. } => rewrite_select_clock(subquery, now),
7208 Expr::InSubquery { expr, subquery, .. } => {
7209 rewrite_expr_clock(expr, now);
7210 rewrite_select_clock(subquery, now);
7211 }
7212 Expr::WindowFunction {
7215 args,
7216 partition_by,
7217 order_by,
7218 ..
7219 } => {
7220 for a in args {
7221 rewrite_expr_clock(a, now);
7222 }
7223 for p in partition_by {
7224 rewrite_expr_clock(p, now);
7225 }
7226 for (e, _) in order_by {
7227 rewrite_expr_clock(e, now);
7228 }
7229 }
7230 Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {}
7231 }
7232}
7233
7234fn clock_replacement_for(e: &Expr, now: i64) -> Option<Expr> {
7241 let (kind, name) = match e {
7242 Expr::FunctionCall { name, args } if args.is_empty() => (ClockSite::Fn, name.as_str()),
7243 Expr::Column(c) if c.qualifier.is_none() => (ClockSite::BareIdent, c.name.as_str()),
7244 _ => return None,
7245 };
7246 let matched = match name.len() {
7249 3 if kind == ClockSite::Fn && name.eq_ignore_ascii_case("now") => Some(true),
7250 12 if name.eq_ignore_ascii_case("current_date") => Some(false),
7251 17 if name.eq_ignore_ascii_case("current_timestamp") => Some(true),
7252 _ => None,
7253 };
7254 let is_timestamp = matched?;
7255 let payload = if is_timestamp {
7256 now
7257 } else {
7258 now.div_euclid(86_400_000_000)
7259 };
7260 let target = if is_timestamp {
7261 spg_sql::ast::CastTarget::Timestamp
7262 } else {
7263 spg_sql::ast::CastTarget::Date
7264 };
7265 Some(Expr::Cast {
7266 expr: alloc::boxed::Box::new(Expr::Literal(spg_sql::ast::Literal::Integer(payload))),
7267 target,
7268 })
7269}
7270
7271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7272enum ClockSite {
7273 Fn,
7274 BareIdent,
7275}
7276
7277fn expand_group_by_all(s: &mut SelectStatement) {
7288 if !s.group_by_all {
7289 for (_, peer) in &mut s.unions {
7290 expand_group_by_all(peer);
7291 }
7292 return;
7293 }
7294 let mut groups: Vec<Expr> = Vec::new();
7295 for item in &s.items {
7296 if let SelectItem::Expr { expr, .. } = item
7297 && !aggregate::contains_aggregate(expr)
7298 {
7299 groups.push(expr.clone());
7300 }
7301 }
7302 s.group_by = Some(groups);
7303 s.group_by_all = false;
7304 for (_, peer) in &mut s.unions {
7305 expand_group_by_all(peer);
7306 }
7307}
7308
7309fn resolve_order_by_position(s: &mut SelectStatement) {
7310 for order in &mut s.order_by {
7315 match &order.expr {
7316 Expr::Literal(Literal::Integer(n)) if *n >= 1 => {
7317 if let Ok(idx_one_based) = usize::try_from(*n) {
7318 let idx = idx_one_based - 1;
7319 if idx < s.items.len()
7320 && let SelectItem::Expr { expr, .. } = &s.items[idx]
7321 {
7322 order.expr = expr.clone();
7323 }
7324 }
7325 }
7326 Expr::Column(c) if c.qualifier.is_none() => {
7327 for item in &s.items {
7329 if let SelectItem::Expr {
7330 expr,
7331 alias: Some(a),
7332 } = item
7333 && a == &c.name
7334 {
7335 order.expr = expr.clone();
7336 break;
7337 }
7338 }
7339 }
7340 _ => {}
7341 }
7342 }
7343 for (_, peer) in &mut s.unions {
7344 resolve_order_by_position(peer);
7345 }
7346}
7347
7348fn partial_sort_tagged(
7361 tagged: &mut Vec<(Vec<f64>, Row)>,
7362 keep: Option<usize>,
7363 descs: &[bool],
7364) {
7365 let cmp = |a: &(Vec<f64>, Row), b: &(Vec<f64>, Row)| cmp_multi_key(&a.0, &b.0, descs);
7366 match keep {
7367 Some(k) if k < tagged.len() && k > 0 => {
7368 let pivot = k - 1;
7369 tagged.select_nth_unstable_by(pivot, cmp);
7370 tagged[..k].sort_by(cmp);
7371 tagged.truncate(k);
7372 }
7373 _ => {
7374 tagged.sort_by(cmp);
7375 }
7376 }
7377}
7378
7379fn sort_by_keys(tagged: &mut [(Vec<f64>, Row)], descs: &[bool]) {
7380 tagged.sort_by(|a, b| cmp_multi_key(&a.0, &b.0, descs));
7381}
7382
7383fn cmp_multi_key(a: &[f64], b: &[f64], descs: &[bool]) -> core::cmp::Ordering {
7387 use core::cmp::Ordering;
7388 for (i, (ka, kb)) in a.iter().zip(b.iter()).enumerate() {
7389 let ord = ka.partial_cmp(kb).unwrap_or(Ordering::Equal);
7390 let ord = if descs.get(i).copied().unwrap_or(false) {
7391 ord.reverse()
7392 } else {
7393 ord
7394 };
7395 if ord != Ordering::Equal {
7396 return ord;
7397 }
7398 }
7399 Ordering::Equal
7400}
7401
7402fn build_order_keys(
7405 order_by: &[OrderBy],
7406 row: &Row,
7407 ctx: &EvalContext,
7408) -> Result<Vec<f64>, EngineError> {
7409 let mut keys = Vec::with_capacity(order_by.len());
7410 for o in order_by {
7411 let v = eval::eval_expr(&o.expr, row, ctx)?;
7412 keys.push(value_to_order_key(&v)?);
7413 }
7414 Ok(keys)
7415}
7416
7417fn apply_offset_and_limit(rows: &mut Vec<Row>, offset: Option<u32>, limit: Option<u32>) {
7421 if let Some(off) = offset {
7422 let off = off as usize;
7423 if off >= rows.len() {
7424 rows.clear();
7425 } else {
7426 rows.drain(..off);
7427 }
7428 }
7429 if let Some(n) = limit {
7430 rows.truncate(n as usize);
7431 }
7432}
7433
7434fn resolve_foreign_key(
7448 local_table_name: &str,
7449 local_cols: &[ColumnSchema],
7450 fk: spg_sql::ast::ForeignKeyConstraint,
7451 catalog: &Catalog,
7452) -> Result<spg_storage::ForeignKeyConstraint, EngineError> {
7453 let mut local_columns = Vec::with_capacity(fk.columns.len());
7455 for name in &fk.columns {
7456 let pos = local_cols
7457 .iter()
7458 .position(|c| c.name == *name)
7459 .ok_or_else(|| {
7460 EngineError::Unsupported(alloc::format!(
7461 "FOREIGN KEY references unknown local column {name:?}"
7462 ))
7463 })?;
7464 local_columns.push(pos);
7465 }
7466 let is_self_ref = fk.parent_table == local_table_name;
7470 let (parent_cols_for_lookup, parent_table_str): (&[ColumnSchema], &str) = if is_self_ref {
7471 (local_cols, local_table_name)
7472 } else {
7473 let parent_table = catalog.get(&fk.parent_table).ok_or_else(|| {
7474 EngineError::Storage(StorageError::TableNotFound {
7475 name: fk.parent_table.clone(),
7476 })
7477 })?;
7478 (parent_table.schema().columns.as_slice(), fk.parent_table.as_str())
7479 };
7480 let parent_columns: Vec<usize> = if fk.parent_columns.is_empty() {
7485 if fk.columns.len() != 1 {
7486 return Err(EngineError::Unsupported(
7487 "composite FOREIGN KEY without explicit parent column list is not supported \
7488 — list the parent columns explicitly"
7489 .into(),
7490 ));
7491 }
7492 let pos = pick_pk_index_column(catalog, parent_table_str, is_self_ref, local_cols)
7494 .ok_or_else(|| {
7495 EngineError::Unsupported(alloc::format!(
7496 "parent table {parent_table_str:?} has no PRIMARY-key / UNIQUE BTree index \
7497 to default the FOREIGN KEY against"
7498 ))
7499 })?;
7500 alloc::vec![pos]
7501 } else {
7502 let mut out = Vec::with_capacity(fk.parent_columns.len());
7503 for name in &fk.parent_columns {
7504 let pos = parent_cols_for_lookup
7505 .iter()
7506 .position(|c| c.name == *name)
7507 .ok_or_else(|| {
7508 EngineError::Unsupported(alloc::format!(
7509 "FOREIGN KEY references unknown parent column \
7510 {name:?} on table {parent_table_str:?}"
7511 ))
7512 })?;
7513 out.push(pos);
7514 }
7515 out
7516 };
7517 if parent_columns.len() != local_columns.len() {
7518 return Err(EngineError::Unsupported(alloc::format!(
7519 "FOREIGN KEY arity mismatch: {} local columns vs {} parent columns",
7520 local_columns.len(),
7521 parent_columns.len()
7522 )));
7523 }
7524 if !is_self_ref {
7534 let parent_table = catalog
7535 .get(&fk.parent_table)
7536 .expect("checked above");
7537 let primary_parent_col = parent_columns[0];
7538 let has_btree = parent_table.schema().columns.get(primary_parent_col).is_some()
7539 && parent_table
7540 .indices()
7541 .iter()
7542 .any(|idx| {
7543 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7544 && idx.column_position == primary_parent_col
7545 && idx.partial_predicate.is_none()
7546 });
7547 if !has_btree {
7548 return Err(EngineError::Unsupported(alloc::format!(
7549 "FOREIGN KEY parent column on {:?} is not covered by an unconditional BTree \
7550 index — create one with `CREATE INDEX ... ON {} ({})` first",
7551 parent_table_str,
7552 parent_table_str,
7553 parent_table.schema().columns[primary_parent_col].name,
7554 )));
7555 }
7556 }
7557 let on_delete = fk_action_sql_to_storage(fk.on_delete);
7558 let on_update = fk_action_sql_to_storage(fk.on_update);
7559 Ok(spg_storage::ForeignKeyConstraint {
7560 name: fk.name,
7561 local_columns,
7562 parent_table: fk.parent_table,
7563 parent_columns,
7564 on_delete,
7565 on_update,
7566 })
7567}
7568
7569fn pick_pk_index_column(
7575 catalog: &Catalog,
7576 parent_name: &str,
7577 is_self_ref: bool,
7578 local_cols: &[ColumnSchema],
7579) -> Option<usize> {
7580 if is_self_ref {
7581 let _ = local_cols;
7585 return Some(0);
7586 }
7587 let parent = catalog.get(parent_name)?;
7588 parent.indices().iter().find_map(|idx| {
7589 if matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7590 && idx.partial_predicate.is_none()
7591 && idx.included_columns.is_empty()
7592 && idx.expression.is_none()
7593 {
7594 Some(idx.column_position)
7595 } else {
7596 None
7597 }
7598 })
7599}
7600
7601fn resolve_on_conflict_columns(
7608 catalog: &Catalog,
7609 table_name: &str,
7610 target: &[String],
7611) -> Result<Vec<usize>, EngineError> {
7612 let table = catalog.get(table_name).ok_or_else(|| {
7613 EngineError::Storage(StorageError::TableNotFound {
7614 name: table_name.into(),
7615 })
7616 })?;
7617 if target.is_empty() {
7618 let pos = table
7619 .indices()
7620 .iter()
7621 .find_map(|idx| {
7622 if matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7623 && idx.partial_predicate.is_none()
7624 && idx.included_columns.is_empty()
7625 && idx.expression.is_none()
7626 {
7627 Some(idx.column_position)
7628 } else {
7629 None
7630 }
7631 })
7632 .ok_or_else(|| {
7633 EngineError::Unsupported(alloc::format!(
7634 "ON CONFLICT without target requires a UNIQUE BTree index on {table_name:?}"
7635 ))
7636 })?;
7637 return Ok(alloc::vec![pos]);
7638 }
7639 let mut out = Vec::with_capacity(target.len());
7640 for name in target {
7641 let pos = table
7642 .schema()
7643 .columns
7644 .iter()
7645 .position(|c| c.name == *name)
7646 .ok_or_else(|| {
7647 EngineError::Unsupported(alloc::format!(
7648 "ON CONFLICT target column {name:?} not found on {table_name:?}"
7649 ))
7650 })?;
7651 out.push(pos);
7652 }
7653 Ok(out)
7654}
7655
7656fn on_conflict_key_exists(
7659 catalog: &Catalog,
7660 table_name: &str,
7661 column_pos: usize,
7662 key: &Value,
7663) -> bool {
7664 let Some(table) = catalog.get(table_name) else {
7665 return false;
7666 };
7667 let Some(idx_key) = spg_storage::IndexKey::from_value(key) else {
7668 return false;
7669 };
7670 table.indices().iter().any(|idx| {
7671 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7672 && idx.column_position == column_pos
7673 && idx.partial_predicate.is_none()
7674 && !idx.lookup_eq(&idx_key).is_empty()
7675 })
7676}
7677
7678fn lookup_row_position_by_keys(
7684 catalog: &Catalog,
7685 table_name: &str,
7686 column_positions: &[usize],
7687 key: &[&Value],
7688) -> Option<usize> {
7689 let table = catalog.get(table_name)?;
7690 table.rows().iter().position(|r| {
7691 column_positions
7692 .iter()
7693 .enumerate()
7694 .all(|(i, &pos)| r.values.get(pos) == Some(key[i]))
7695 })
7696}
7697
7698fn on_conflict_keys_exist(
7703 catalog: &Catalog,
7704 table_name: &str,
7705 column_positions: &[usize],
7706 key: &[&Value],
7707) -> bool {
7708 if column_positions.len() == 1 {
7709 return on_conflict_key_exists(
7710 catalog,
7711 table_name,
7712 column_positions[0],
7713 key[0],
7714 );
7715 }
7716 let Some(table) = catalog.get(table_name) else {
7717 return false;
7718 };
7719 table.rows().iter().any(|r| {
7720 column_positions
7721 .iter()
7722 .enumerate()
7723 .all(|(i, &pos)| r.values.get(pos) == Some(key[i]))
7724 })
7725}
7726
7727fn apply_on_conflict_assignments(
7740 catalog: &Catalog,
7741 table_name: &str,
7742 target_pos: usize,
7743 incoming: &[Value],
7744 assignments: &[(String, Expr)],
7745 where_: Option<&Expr>,
7746) -> Result<Option<Vec<Value>>, EngineError> {
7747 let table = catalog.get(table_name).ok_or_else(|| {
7748 EngineError::Storage(StorageError::TableNotFound {
7749 name: table_name.into(),
7750 })
7751 })?;
7752 let schema_cols = table.schema().columns.clone();
7753 let existing = table
7754 .rows()
7755 .get(target_pos)
7756 .ok_or_else(|| {
7757 EngineError::Unsupported(alloc::format!(
7758 "ON CONFLICT DO UPDATE: row position {target_pos} out of bounds on {table_name:?}"
7759 ))
7760 })?
7761 .clone();
7762 let ctx = eval::EvalContext::new(&schema_cols, Some(table_name));
7763 if let Some(w) = where_ {
7765 let pred = w.clone();
7766 let pred = substitute_excluded_refs(pred, &schema_cols, incoming);
7767 let v = eval::eval_expr(&pred, &existing, &ctx)?;
7768 if !matches!(v, Value::Bool(true)) {
7769 return Ok(None);
7770 }
7771 }
7772 let mut new_values = existing.values.clone();
7773 for (col_name, expr) in assignments {
7774 let target_idx = schema_cols
7775 .iter()
7776 .position(|c| c.name == *col_name)
7777 .ok_or_else(|| {
7778 EngineError::Eval(EvalError::ColumnNotFound {
7779 name: col_name.clone(),
7780 })
7781 })?;
7782 let sub = substitute_excluded_refs(expr.clone(), &schema_cols, incoming);
7783 let v = eval::eval_expr(&sub, &existing, &ctx)?;
7784 new_values[target_idx] =
7785 coerce_value(v, schema_cols[target_idx].ty, col_name, target_idx)?;
7786 }
7787 Ok(Some(new_values))
7788}
7789
7790fn substitute_excluded_refs(
7795 expr: Expr,
7796 schema_cols: &[ColumnSchema],
7797 incoming: &[Value],
7798) -> Expr {
7799 use spg_sql::ast::ColumnName;
7800 match expr {
7801 Expr::Column(ColumnName { qualifier, name })
7802 if qualifier
7803 .as_deref()
7804 .is_some_and(|q| q.eq_ignore_ascii_case("excluded")) =>
7805 {
7806 let pos = schema_cols.iter().position(|c| c.name == name);
7807 match pos {
7808 Some(p) => {
7809 let v = incoming.get(p).cloned().unwrap_or(Value::Null);
7810 value_to_literal_expr(v).unwrap_or_else(|_| {
7811 Expr::Literal(spg_sql::ast::Literal::Null)
7812 })
7813 }
7814 None => Expr::Column(ColumnName { qualifier, name }),
7815 }
7816 }
7817 Expr::Binary { op, lhs, rhs } => Expr::Binary {
7818 op,
7819 lhs: Box::new(substitute_excluded_refs(*lhs, schema_cols, incoming)),
7820 rhs: Box::new(substitute_excluded_refs(*rhs, schema_cols, incoming)),
7821 },
7822 Expr::Unary { op, expr } => Expr::Unary {
7823 op,
7824 expr: Box::new(substitute_excluded_refs(*expr, schema_cols, incoming)),
7825 },
7826 Expr::FunctionCall { name, args } => Expr::FunctionCall {
7827 name,
7828 args: args
7829 .into_iter()
7830 .map(|a| substitute_excluded_refs(a, schema_cols, incoming))
7831 .collect(),
7832 },
7833 other => other,
7834 }
7835}
7836
7837fn enforce_uniqueness_inserts(
7860 catalog: &Catalog,
7861 child_table: &str,
7862 constraints: &[spg_storage::UniquenessConstraint],
7863 rows: &[Vec<Value>],
7864) -> Result<(), EngineError> {
7865 if constraints.is_empty() {
7866 return Ok(());
7867 }
7868 let table = catalog.get(child_table).ok_or_else(|| {
7869 EngineError::Storage(StorageError::TableNotFound {
7870 name: child_table.into(),
7871 })
7872 })?;
7873 for uc in constraints {
7874 for (batch_idx, row_values) in rows.iter().enumerate() {
7875 let key: Vec<&Value> = uc.columns.iter().map(|&i| &row_values[i]).collect();
7876 let has_null = key.iter().any(|v| matches!(v, Value::Null));
7877 if has_null {
7878 continue;
7879 }
7880 let collides_in_table = table.rows().iter().any(|prow| {
7882 uc.columns
7883 .iter()
7884 .enumerate()
7885 .all(|(i, &p)| prow.values.get(p) == Some(key[i]))
7886 });
7887 let collides_in_batch = rows[..batch_idx].iter().any(|earlier| {
7889 uc.columns
7890 .iter()
7891 .enumerate()
7892 .all(|(i, &p)| earlier.get(p) == Some(key[i]))
7893 });
7894 if collides_in_table || collides_in_batch {
7895 let kind = if uc.is_primary_key { "PRIMARY KEY" } else { "UNIQUE" };
7896 let col_names: Vec<String> = uc
7897 .columns
7898 .iter()
7899 .map(|&i| table.schema().columns[i].name.clone())
7900 .collect();
7901 return Err(EngineError::Unsupported(alloc::format!(
7902 "{kind} violation on {child_table:?} columns {col_names:?}: \
7903 row #{batch_idx} duplicates an existing key"
7904 )));
7905 }
7906 }
7907 }
7908 Ok(())
7909}
7910
7911fn enforce_fk_inserts(
7912 catalog: &Catalog,
7913 child_table: &str,
7914 fks: &[spg_storage::ForeignKeyConstraint],
7915 rows: &[Vec<Value>],
7916) -> Result<(), EngineError> {
7917 for fk in fks {
7918 let parent_is_self = fk.parent_table == child_table;
7919 let parent = if parent_is_self {
7920 catalog.get(child_table).ok_or_else(|| {
7923 EngineError::Storage(StorageError::TableNotFound {
7924 name: child_table.into(),
7925 })
7926 })?
7927 } else {
7928 catalog.get(&fk.parent_table).ok_or_else(|| {
7929 EngineError::Storage(StorageError::TableNotFound {
7930 name: fk.parent_table.clone(),
7931 })
7932 })?
7933 };
7934 for (batch_idx, row_values) in rows.iter().enumerate() {
7935 if fk.local_columns.len() == 1 {
7939 let v = &row_values[fk.local_columns[0]];
7940 if matches!(v, Value::Null) {
7941 continue;
7942 }
7943 let parent_col = fk.parent_columns[0];
7944 let key = spg_storage::IndexKey::from_value(v).ok_or_else(|| {
7945 EngineError::Unsupported(alloc::format!(
7946 "FOREIGN KEY column value of type {:?} is not index-eligible",
7947 v.data_type()
7948 ))
7949 })?;
7950 let present_committed = parent.indices().iter().any(|idx| {
7951 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7952 && idx.column_position == parent_col
7953 && idx.partial_predicate.is_none()
7954 && !idx.lookup_eq(&key).is_empty()
7955 });
7956 let present_in_batch = parent_is_self
7960 && rows[..batch_idx].iter().any(|earlier| {
7961 earlier.get(parent_col) == Some(v)
7962 });
7963 if !(present_committed || present_in_batch) {
7964 return Err(EngineError::Unsupported(alloc::format!(
7965 "FOREIGN KEY violation: no parent row in {:?} where {} = {:?}",
7966 fk.parent_table,
7967 parent
7968 .schema()
7969 .columns
7970 .get(parent_col)
7971 .map_or("?", |c| c.name.as_str()),
7972 v,
7973 )));
7974 }
7975 } else {
7976 if fk.local_columns
7980 .iter()
7981 .all(|&i| matches!(row_values.get(i), Some(Value::Null)))
7982 {
7983 continue;
7984 }
7985 let local: Vec<&Value> = fk.local_columns.iter().map(|&i| &row_values[i]).collect();
7986 let parent_match_committed = parent.rows().iter().any(|prow| {
7987 fk.parent_columns
7988 .iter()
7989 .enumerate()
7990 .all(|(i, &pi)| prow.values.get(pi) == Some(local[i]))
7991 });
7992 let parent_match_in_batch = parent_is_self
7993 && rows[..batch_idx].iter().any(|earlier| {
7994 fk.parent_columns
7995 .iter()
7996 .enumerate()
7997 .all(|(i, &pi)| earlier.get(pi) == Some(local[i]))
7998 });
7999 if !(parent_match_committed || parent_match_in_batch) {
8000 return Err(EngineError::Unsupported(alloc::format!(
8001 "FOREIGN KEY violation: no parent row in {:?} matching composite key",
8002 fk.parent_table,
8003 )));
8004 }
8005 }
8006 }
8007 }
8008 Ok(())
8009}
8010
8011#[derive(Debug, Clone)]
8015struct FkChildStep {
8016 child_table: String,
8017 action: FkChildAction,
8018}
8019
8020#[derive(Debug, Clone)]
8021enum FkChildAction {
8022 Delete { positions: Vec<usize> },
8024 SetNull {
8028 positions: Vec<usize>,
8029 columns: Vec<usize>,
8030 },
8031 SetDefault {
8035 positions: Vec<usize>,
8036 columns: Vec<usize>,
8037 defaults: Vec<Value>,
8038 },
8039}
8040
8041fn plan_fk_parent_deletions(
8057 catalog: &Catalog,
8058 parent_table_name: &str,
8059 to_delete_positions: &[usize],
8060 to_delete_rows: &[Vec<Value>],
8061) -> Result<Vec<FkChildStep>, EngineError> {
8062 use alloc::collections::{BTreeMap, BTreeSet};
8063 if to_delete_rows.is_empty() {
8064 return Ok(Vec::new());
8065 }
8066 let mut delete_plan: BTreeMap<String, BTreeSet<usize>> = BTreeMap::new();
8067 let mut setnull_plan: BTreeMap<String, BTreeSet<(usize, usize)>> = BTreeMap::new();
8069 let mut setdefault_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> =
8070 BTreeMap::new();
8071 let mut visited: BTreeSet<(String, usize)> = BTreeSet::new();
8072 for &p in to_delete_positions {
8073 visited.insert((parent_table_name.to_string(), p));
8074 }
8075 let mut work: Vec<(String, Vec<Value>)> = to_delete_rows
8076 .iter()
8077 .map(|r| (parent_table_name.to_string(), r.clone()))
8078 .collect();
8079 while let Some((cur_parent, parent_row)) = work.pop() {
8080 for child_name in catalog.table_names() {
8081 let child = catalog
8082 .get(&child_name)
8083 .expect("table_names → catalog.get round-trip is total");
8084 for fk in &child.schema().foreign_keys {
8085 if fk.parent_table != cur_parent {
8086 continue;
8087 }
8088 let parent_key: Vec<&Value> = fk
8089 .parent_columns
8090 .iter()
8091 .map(|&pi| &parent_row[pi])
8092 .collect();
8093 if parent_key.iter().any(|v| matches!(v, Value::Null)) {
8094 continue;
8095 }
8096 for (child_row_idx, child_row) in child.rows().iter().enumerate() {
8097 if child_name == cur_parent
8098 && visited.contains(&(child_name.clone(), child_row_idx))
8099 {
8100 continue;
8101 }
8102 let matches_key = fk
8103 .local_columns
8104 .iter()
8105 .enumerate()
8106 .all(|(i, &li)| child_row.values.get(li) == Some(parent_key[i]));
8107 if !matches_key {
8108 continue;
8109 }
8110 match fk.on_delete {
8111 spg_storage::FkAction::Restrict
8112 | spg_storage::FkAction::NoAction => {
8113 return Err(EngineError::Unsupported(alloc::format!(
8114 "FOREIGN KEY violation: DELETE on {cur_parent:?} is \
8115 restricted by FK from {child_name:?}.{:?}",
8116 fk.local_columns,
8117 )));
8118 }
8119 spg_storage::FkAction::Cascade => {
8120 if visited.insert((child_name.clone(), child_row_idx)) {
8121 delete_plan
8122 .entry(child_name.clone())
8123 .or_default()
8124 .insert(child_row_idx);
8125 work.push((child_name.clone(), child_row.values.clone()));
8126 }
8127 }
8128 spg_storage::FkAction::SetNull => {
8129 for &li in &fk.local_columns {
8131 let col = child.schema().columns.get(li).ok_or_else(|| {
8132 EngineError::Unsupported(alloc::format!(
8133 "FK local column {li} missing in {child_name:?}"
8134 ))
8135 })?;
8136 if !col.nullable {
8137 return Err(EngineError::Unsupported(alloc::format!(
8138 "FOREIGN KEY ON DELETE SET NULL: column \
8139 {child_name:?}.{:?} is NOT NULL — cannot SET NULL",
8140 col.name,
8141 )));
8142 }
8143 }
8144 let entry = setnull_plan.entry(child_name.clone()).or_default();
8145 for &li in &fk.local_columns {
8146 entry.insert((child_row_idx, li));
8147 }
8148 }
8149 spg_storage::FkAction::SetDefault => {
8150 let entry =
8152 setdefault_plan.entry(child_name.clone()).or_default();
8153 for &li in &fk.local_columns {
8154 let col = child.schema().columns.get(li).ok_or_else(|| {
8155 EngineError::Unsupported(alloc::format!(
8156 "FK local column {li} missing in {child_name:?}"
8157 ))
8158 })?;
8159 let default = col.default.clone().ok_or_else(|| {
8160 EngineError::Unsupported(alloc::format!(
8161 "FOREIGN KEY ON DELETE SET DEFAULT: column \
8162 {child_name:?}.{:?} has no DEFAULT declared",
8163 col.name,
8164 ))
8165 })?;
8166 entry.insert((child_row_idx, li), default);
8167 }
8168 }
8169 }
8170 }
8171 }
8172 }
8173 }
8174 let mut steps: Vec<FkChildStep> = Vec::new();
8182 for (child_table, entries) in setnull_plan {
8183 let (positions, columns): (Vec<usize>, Vec<usize>) = entries.into_iter().unzip();
8184 steps.push(FkChildStep {
8185 child_table,
8186 action: FkChildAction::SetNull { positions, columns },
8187 });
8188 }
8189 for (child_table, entries) in setdefault_plan {
8190 let mut positions = Vec::with_capacity(entries.len());
8191 let mut columns = Vec::with_capacity(entries.len());
8192 let mut defaults = Vec::with_capacity(entries.len());
8193 for ((p, c), v) in entries {
8194 positions.push(p);
8195 columns.push(c);
8196 defaults.push(v);
8197 }
8198 steps.push(FkChildStep {
8199 child_table,
8200 action: FkChildAction::SetDefault {
8201 positions,
8202 columns,
8203 defaults,
8204 },
8205 });
8206 }
8207 for (child_table, positions) in delete_plan {
8208 steps.push(FkChildStep {
8209 child_table,
8210 action: FkChildAction::Delete {
8211 positions: positions.into_iter().collect(),
8212 },
8213 });
8214 }
8215 Ok(steps)
8216}
8217
8218fn plan_fk_parent_updates(
8235 catalog: &Catalog,
8236 parent_table_name: &str,
8237 plan_with_old: &[(usize, Vec<Value>, Vec<Value>)],
8238) -> Result<Vec<FkChildStep>, EngineError> {
8239 use alloc::collections::BTreeMap;
8240 if plan_with_old.is_empty() {
8241 return Ok(Vec::new());
8242 }
8243 let delete_plan: BTreeMap<String, alloc::collections::BTreeSet<usize>> = BTreeMap::new();
8248 let mut setnull_plan: BTreeMap<
8249 String,
8250 alloc::collections::BTreeSet<(usize, usize)>,
8251 > = BTreeMap::new();
8252 let mut setdefault_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> =
8253 BTreeMap::new();
8254 let mut cascade_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> = BTreeMap::new();
8256
8257 for child_name in catalog.table_names() {
8258 let child = catalog
8259 .get(&child_name)
8260 .expect("table_names → catalog.get total");
8261 for fk in &child.schema().foreign_keys {
8262 if fk.parent_table != parent_table_name {
8263 continue;
8264 }
8265 for (_pos, old_row, new_row) in plan_with_old {
8266 let key_changed = fk
8268 .parent_columns
8269 .iter()
8270 .any(|&pi| old_row.get(pi) != new_row.get(pi));
8271 if !key_changed {
8272 continue;
8273 }
8274 let old_key: Vec<&Value> = fk
8276 .parent_columns
8277 .iter()
8278 .map(|&pi| &old_row[pi])
8279 .collect();
8280 if old_key.iter().any(|v| matches!(v, Value::Null)) {
8281 continue;
8283 }
8284 let new_key: Vec<&Value> = fk
8285 .parent_columns
8286 .iter()
8287 .map(|&pi| &new_row[pi])
8288 .collect();
8289 for (child_row_idx, child_row) in child.rows().iter().enumerate() {
8290 if child_name == parent_table_name
8293 && plan_with_old
8294 .iter()
8295 .any(|(p, _, _)| *p == child_row_idx)
8296 {
8297 continue;
8298 }
8299 let matches_key = fk
8300 .local_columns
8301 .iter()
8302 .enumerate()
8303 .all(|(i, &li)| child_row.values.get(li) == Some(old_key[i]));
8304 if !matches_key {
8305 continue;
8306 }
8307 match fk.on_update {
8308 spg_storage::FkAction::Restrict
8309 | spg_storage::FkAction::NoAction => {
8310 return Err(EngineError::Unsupported(alloc::format!(
8311 "FOREIGN KEY violation: UPDATE on {parent_table_name:?} PK is \
8312 restricted by FK from {child_name:?}.{:?}",
8313 fk.local_columns,
8314 )));
8315 }
8316 spg_storage::FkAction::Cascade => {
8317 let entry = cascade_plan.entry(child_name.clone()).or_default();
8319 for (i, &li) in fk.local_columns.iter().enumerate() {
8320 entry.insert((child_row_idx, li), new_key[i].clone());
8321 }
8322 }
8323 spg_storage::FkAction::SetNull => {
8324 for &li in &fk.local_columns {
8325 let col = child.schema().columns.get(li).ok_or_else(|| {
8326 EngineError::Unsupported(alloc::format!(
8327 "FK local column {li} missing in {child_name:?}"
8328 ))
8329 })?;
8330 if !col.nullable {
8331 return Err(EngineError::Unsupported(alloc::format!(
8332 "FOREIGN KEY ON UPDATE SET NULL: column \
8333 {child_name:?}.{:?} is NOT NULL",
8334 col.name,
8335 )));
8336 }
8337 }
8338 let entry = setnull_plan.entry(child_name.clone()).or_default();
8339 for &li in &fk.local_columns {
8340 entry.insert((child_row_idx, li));
8341 }
8342 }
8343 spg_storage::FkAction::SetDefault => {
8344 let entry =
8345 setdefault_plan.entry(child_name.clone()).or_default();
8346 for &li in &fk.local_columns {
8347 let col = child.schema().columns.get(li).ok_or_else(|| {
8348 EngineError::Unsupported(alloc::format!(
8349 "FK local column {li} missing in {child_name:?}"
8350 ))
8351 })?;
8352 let default = col.default.clone().ok_or_else(|| {
8353 EngineError::Unsupported(alloc::format!(
8354 "FOREIGN KEY ON UPDATE SET DEFAULT: column \
8355 {child_name:?}.{:?} has no DEFAULT",
8356 col.name,
8357 ))
8358 })?;
8359 entry.insert((child_row_idx, li), default);
8360 }
8361 }
8362 }
8363 }
8364 }
8365 }
8366 }
8367 let mut steps: Vec<FkChildStep> = Vec::new();
8370 for (child_table, entries) in cascade_plan {
8371 let mut positions = Vec::with_capacity(entries.len());
8372 let mut columns = Vec::with_capacity(entries.len());
8373 let mut defaults = Vec::with_capacity(entries.len());
8374 for ((p, c), v) in entries {
8375 positions.push(p);
8376 columns.push(c);
8377 defaults.push(v);
8378 }
8379 steps.push(FkChildStep {
8384 child_table,
8385 action: FkChildAction::SetDefault {
8386 positions,
8387 columns,
8388 defaults,
8389 },
8390 });
8391 }
8392 for (child_table, entries) in setnull_plan {
8393 let (positions, columns): (Vec<usize>, Vec<usize>) = entries.into_iter().unzip();
8394 steps.push(FkChildStep {
8395 child_table,
8396 action: FkChildAction::SetNull { positions, columns },
8397 });
8398 }
8399 for (child_table, entries) in setdefault_plan {
8400 let mut positions = Vec::with_capacity(entries.len());
8401 let mut columns = Vec::with_capacity(entries.len());
8402 let mut defaults = Vec::with_capacity(entries.len());
8403 for ((p, c), v) in entries {
8404 positions.push(p);
8405 columns.push(c);
8406 defaults.push(v);
8407 }
8408 steps.push(FkChildStep {
8409 child_table,
8410 action: FkChildAction::SetDefault {
8411 positions,
8412 columns,
8413 defaults,
8414 },
8415 });
8416 }
8417 let _ = delete_plan; Ok(steps)
8419}
8420
8421fn apply_fk_child_step(
8425 catalog: &mut Catalog,
8426 step: &FkChildStep,
8427) -> Result<(), EngineError> {
8428 let child = catalog.get_mut(&step.child_table).ok_or_else(|| {
8429 EngineError::Storage(StorageError::TableNotFound {
8430 name: step.child_table.clone(),
8431 })
8432 })?;
8433 match &step.action {
8434 FkChildAction::Delete { positions } => {
8435 let _ = child.delete_rows(positions);
8436 }
8437 FkChildAction::SetNull { positions, columns } => {
8438 apply_per_cell_writes(child, positions, columns, |_| Value::Null)?;
8439 }
8440 FkChildAction::SetDefault {
8441 positions,
8442 columns,
8443 defaults,
8444 } => {
8445 apply_per_cell_writes(child, positions, columns, |i| defaults[i].clone())?;
8446 }
8447 }
8448 Ok(())
8449}
8450
8451fn apply_per_cell_writes(
8457 child: &mut spg_storage::Table,
8458 positions: &[usize],
8459 columns: &[usize],
8460 mut value_for: impl FnMut(usize) -> Value,
8461) -> Result<(), EngineError> {
8462 use alloc::collections::BTreeMap;
8463 let mut by_row: BTreeMap<usize, Vec<(usize, Value)>> = BTreeMap::new();
8464 for i in 0..positions.len() {
8465 by_row
8466 .entry(positions[i])
8467 .or_default()
8468 .push((columns[i], value_for(i)));
8469 }
8470 for (pos, mutations) in by_row {
8471 let mut new_values = child.rows()[pos].values.clone();
8472 for (col, v) in mutations {
8473 if let Some(slot) = new_values.get_mut(col) {
8474 *slot = v;
8475 }
8476 }
8477 child
8478 .update_row(pos, new_values)
8479 .map_err(EngineError::Storage)?;
8480 }
8481 Ok(())
8482}
8483
8484fn fk_action_sql_to_storage(a: spg_sql::ast::FkAction) -> spg_storage::FkAction {
8485 match a {
8486 spg_sql::ast::FkAction::Restrict => spg_storage::FkAction::Restrict,
8487 spg_sql::ast::FkAction::Cascade => spg_storage::FkAction::Cascade,
8488 spg_sql::ast::FkAction::SetNull => spg_storage::FkAction::SetNull,
8489 spg_sql::ast::FkAction::SetDefault => spg_storage::FkAction::SetDefault,
8490 spg_sql::ast::FkAction::NoAction => spg_storage::FkAction::NoAction,
8491 }
8492}
8493
8494fn resolve_column_default_free(
8500 col: &ColumnSchema,
8501 clock_fn: Option<ClockFn>,
8502) -> Result<Value, EngineError> {
8503 if let Some(rt) = &col.runtime_default {
8504 return eval_runtime_default_free(rt, col.ty, clock_fn);
8505 }
8506 Ok(col.default.clone().unwrap_or(Value::Null))
8507}
8508
8509fn eval_runtime_default_free(
8510 rt: &str,
8511 ty: DataType,
8512 clock_fn: Option<ClockFn>,
8513) -> Result<Value, EngineError> {
8514 let s = rt.trim().to_ascii_lowercase();
8515 let canonical = s.trim_end_matches("()");
8516 let now_us = match clock_fn {
8517 Some(f) => f(),
8518 None => 0,
8519 };
8520 let v = match canonical {
8521 "now" | "current_timestamp" | "localtimestamp" => {
8522 Value::Timestamp(now_us)
8523 }
8524 "current_date" => Value::Date((now_us / 86_400_000_000) as i32),
8525 "current_time" | "localtime" => Value::Timestamp(now_us),
8526 other => {
8527 return Err(EngineError::Unsupported(alloc::format!(
8528 "runtime DEFAULT expression {other:?} not supported \
8529 (v7.9.21 whitelist: now() / current_timestamp / \
8530 current_date / current_time / localtimestamp / \
8531 localtime)"
8532 )));
8533 }
8534 };
8535 coerce_value(v, ty, "DEFAULT", 0)
8536}
8537
8538fn is_runtime_default_expr(expr: &Expr) -> bool {
8544 match expr {
8545 Expr::FunctionCall { .. } => true,
8546 Expr::Unary { expr, .. } => is_runtime_default_expr(expr),
8547 _ => false,
8548 }
8549}
8550
8551fn column_def_to_schema(c: ColumnDef) -> Result<ColumnSchema, EngineError> {
8552 let ty = column_type_to_data_type(c.ty);
8553 let mut schema = ColumnSchema::new(c.name.clone(), ty, c.nullable);
8554 if let Some(default_expr) = c.default {
8555 if is_runtime_default_expr(&default_expr) {
8561 let display = alloc::format!("{default_expr}");
8562 schema = schema.with_runtime_default(display);
8563 } else {
8564 let raw = literal_expr_to_value(default_expr)?;
8565 let coerced = coerce_value(raw, ty, &c.name, 0)?;
8566 schema = schema.with_default(coerced);
8567 }
8568 }
8569 if c.auto_increment {
8570 if !matches!(ty, DataType::SmallInt | DataType::Int | DataType::BigInt) {
8572 return Err(EngineError::Unsupported(alloc::format!(
8573 "AUTO_INCREMENT requires an integer column type, got {ty:?}"
8574 )));
8575 }
8576 schema = schema.with_auto_increment();
8577 }
8578 Ok(schema)
8579}
8580
8581const fn column_type_to_data_type(t: ColumnTypeName) -> DataType {
8582 match t {
8583 ColumnTypeName::SmallInt => DataType::SmallInt,
8584 ColumnTypeName::Int => DataType::Int,
8585 ColumnTypeName::BigInt => DataType::BigInt,
8586 ColumnTypeName::Float => DataType::Float,
8587 ColumnTypeName::Text => DataType::Text,
8588 ColumnTypeName::Varchar(n) => DataType::Varchar(n),
8589 ColumnTypeName::Char(n) => DataType::Char(n),
8590 ColumnTypeName::Bool => DataType::Bool,
8591 ColumnTypeName::Vector { dim, encoding } => DataType::Vector {
8592 dim,
8593 encoding: match encoding {
8594 SqlVecEncoding::F32 => VecEncoding::F32,
8595 SqlVecEncoding::Sq8 => VecEncoding::Sq8,
8596 SqlVecEncoding::F16 => VecEncoding::F16,
8597 },
8598 },
8599 ColumnTypeName::Numeric(precision, scale) => DataType::Numeric { precision, scale },
8600 ColumnTypeName::Date => DataType::Date,
8601 ColumnTypeName::Timestamp => DataType::Timestamp,
8602 ColumnTypeName::Timestamptz => DataType::Timestamptz,
8603 ColumnTypeName::Json => DataType::Json,
8604 ColumnTypeName::Jsonb => DataType::Jsonb,
8605 }
8606}
8607
8608fn literal_expr_to_value(expr: Expr) -> Result<Value, EngineError> {
8612 match expr {
8613 Expr::Literal(l) => Ok(literal_to_value(l)),
8614 Expr::Cast { expr, target } => {
8615 let inner_value = literal_expr_to_value(*expr)?;
8616 crate::eval::cast_value(inner_value, target).map_err(EngineError::Eval)
8617 }
8618 Expr::Unary {
8619 op: UnOp::Neg,
8620 expr,
8621 } => match *expr {
8622 Expr::Literal(Literal::Integer(n)) => {
8623 let neg = n.checked_neg().ok_or_else(|| {
8626 EngineError::Unsupported("integer literal overflow on negation".into())
8627 })?;
8628 Ok(int_value_for(neg))
8629 }
8630 Expr::Literal(Literal::Float(x)) => Ok(Value::Float(-x)),
8631 other => Err(EngineError::Unsupported(alloc::format!(
8632 "unary minus over non-literal expression: {other:?}"
8633 ))),
8634 },
8635 other => Err(EngineError::Unsupported(alloc::format!(
8636 "non-literal INSERT value expression: {other:?}"
8637 ))),
8638 }
8639}
8640
8641fn literal_to_value(l: Literal) -> Value {
8642 match l {
8643 Literal::Integer(n) => int_value_for(n),
8644 Literal::Float(x) => Value::Float(x),
8645 Literal::String(s) => Value::Text(s),
8646 Literal::Bool(b) => Value::Bool(b),
8647 Literal::Null => Value::Null,
8648 Literal::Vector(v) => Value::Vector(v),
8649 Literal::Interval { months, micros, .. } => Value::Interval { months, micros },
8650 }
8651}
8652
8653fn int_value_for(n: i64) -> Value {
8657 if let Ok(small) = i32::try_from(n) {
8658 Value::Int(small)
8659 } else {
8660 Value::BigInt(n)
8661 }
8662}
8663
8664#[allow(clippy::too_many_lines)]
8670fn coerce_value(
8671 v: Value,
8672 expected: DataType,
8673 col_name: &str,
8674 position: usize,
8675) -> Result<Value, EngineError> {
8676 if v.is_null() {
8677 return Ok(Value::Null);
8678 }
8679 let actual = v.data_type().expect("non-null");
8680 if actual == expected {
8681 return Ok(v);
8682 }
8683 let coerced =
8684 match (v, expected) {
8685 (Value::Int(n), DataType::BigInt) => Some(Value::BigInt(i64::from(n))),
8686 (Value::Int(n), DataType::Float) => Some(Value::Float(f64::from(n))),
8687 (Value::Int(n), DataType::SmallInt) => i16::try_from(n).ok().map(Value::SmallInt),
8688 (Value::Int(n), DataType::Numeric { precision, scale }) => Some(numeric_from_integer(
8689 i128::from(n),
8690 precision,
8691 scale,
8692 col_name,
8693 )?),
8694 (Value::SmallInt(n), DataType::Int) => Some(Value::Int(i32::from(n))),
8695 (Value::SmallInt(n), DataType::BigInt) => Some(Value::BigInt(i64::from(n))),
8696 (Value::SmallInt(n), DataType::Float) => Some(Value::Float(f64::from(n))),
8697 (Value::SmallInt(n), DataType::Numeric { precision, scale }) => Some(
8698 numeric_from_integer(i128::from(n), precision, scale, col_name)?,
8699 ),
8700 (Value::BigInt(n), DataType::Int) => i32::try_from(n).ok().map(Value::Int),
8701 (Value::BigInt(n), DataType::SmallInt) => i16::try_from(n).ok().map(Value::SmallInt),
8702 #[allow(clippy::cast_precision_loss)]
8703 (Value::BigInt(n), DataType::Float) => Some(Value::Float(n as f64)),
8704 (Value::BigInt(n), DataType::Numeric { precision, scale }) => Some(
8705 numeric_from_integer(i128::from(n), precision, scale, col_name)?,
8706 ),
8707 (Value::Float(x), DataType::Numeric { precision, scale }) => {
8708 Some(numeric_from_float(x, precision, scale, col_name)?)
8709 }
8710 (Value::Text(s), DataType::Date) => {
8712 let d = eval::parse_date_literal(&s).ok_or_else(|| {
8713 EngineError::Eval(EvalError::TypeMismatch {
8714 detail: alloc::format!(
8715 "cannot parse {s:?} as DATE for column `{col_name}`"
8716 ),
8717 })
8718 })?;
8719 Some(Value::Date(d))
8720 }
8721 (Value::Text(s), DataType::Json | DataType::Jsonb) => Some(Value::Json(s)),
8725 (Value::Json(s), DataType::Text) => Some(Value::Text(s)),
8726 (Value::Text(s), DataType::Timestamp | DataType::Timestamptz) => {
8727 let t = eval::parse_timestamp_literal(&s).ok_or_else(|| {
8728 EngineError::Eval(EvalError::TypeMismatch {
8729 detail: alloc::format!(
8730 "cannot parse {s:?} as TIMESTAMP for column `{col_name}`"
8731 ),
8732 })
8733 })?;
8734 Some(Value::Timestamp(t))
8735 }
8736 (Value::Date(d), DataType::Timestamp | DataType::Timestamptz) => {
8739 Some(Value::Timestamp(i64::from(d) * 86_400_000_000))
8740 }
8741 (Value::Timestamp(t), DataType::Timestamptz) => Some(Value::Timestamp(t)),
8745 (Value::Timestamp(t), DataType::Date) => {
8746 let days = t.div_euclid(86_400_000_000);
8747 i32::try_from(days).ok().map(Value::Date)
8748 }
8749 (
8750 Value::Numeric {
8751 scaled,
8752 scale: src_scale,
8753 },
8754 DataType::Numeric { precision, scale },
8755 ) => Some(numeric_rescale(
8756 scaled, src_scale, precision, scale, col_name,
8757 )?),
8758 #[allow(clippy::cast_precision_loss)]
8759 (Value::Numeric { scaled, scale }, DataType::Float) => {
8760 let mut div = 1.0_f64;
8761 for _ in 0..scale {
8762 div *= 10.0;
8763 }
8764 Some(Value::Float((scaled as f64) / div))
8765 }
8766 (Value::Numeric { scaled, scale }, DataType::Int) => {
8767 let truncated = numeric_truncate_to_integer(scaled, scale);
8768 i32::try_from(truncated).ok().map(Value::Int)
8769 }
8770 (Value::Numeric { scaled, scale }, DataType::BigInt) => {
8771 let truncated = numeric_truncate_to_integer(scaled, scale);
8772 i64::try_from(truncated).ok().map(Value::BigInt)
8773 }
8774 (Value::Numeric { scaled, scale }, DataType::SmallInt) => {
8775 let truncated = numeric_truncate_to_integer(scaled, scale);
8776 i16::try_from(truncated).ok().map(Value::SmallInt)
8777 }
8778 (Value::Text(s), DataType::Varchar(max)) => {
8780 if u32::try_from(s.chars().count()).unwrap_or(u32::MAX) <= max {
8781 Some(Value::Text(s))
8782 } else {
8783 return Err(EngineError::Unsupported(alloc::format!(
8784 "value for VARCHAR({max}) column `{col_name}` exceeds length: \
8785 {} chars",
8786 s.chars().count()
8787 )));
8788 }
8789 }
8790 (
8798 Value::Vector(v),
8799 DataType::Vector {
8800 dim,
8801 encoding: VecEncoding::Sq8,
8802 },
8803 ) if v.len() == dim as usize => {
8804 Some(Value::Sq8Vector(spg_storage::quantize::quantize(&v)))
8805 }
8806 (
8811 Value::Vector(v),
8812 DataType::Vector {
8813 dim,
8814 encoding: VecEncoding::F16,
8815 },
8816 ) if v.len() == dim as usize => Some(Value::HalfVector(
8817 spg_storage::halfvec::HalfVector::from_f32_slice(&v),
8818 )),
8819 (Value::Text(s), DataType::Char(size)) => {
8823 let len = u32::try_from(s.chars().count()).unwrap_or(u32::MAX);
8824 if len > size {
8825 return Err(EngineError::Unsupported(alloc::format!(
8826 "value for CHAR({size}) column `{col_name}` exceeds length: \
8827 {len} chars"
8828 )));
8829 }
8830 let need = (size - len) as usize;
8831 let mut padded = s;
8832 padded.reserve(need);
8833 for _ in 0..need {
8834 padded.push(' ');
8835 }
8836 Some(Value::Text(padded))
8837 }
8838 _ => None,
8839 };
8840 coerced.ok_or(EngineError::Storage(StorageError::TypeMismatch {
8841 column: col_name.into(),
8842 expected,
8843 actual,
8844 position,
8845 }))
8846}
8847
8848#[cfg(test)]
8849mod tests {
8850 use super::*;
8851 use alloc::vec;
8852
8853 fn unwrap_command_ok(r: &QueryResult) -> usize {
8854 match r {
8855 QueryResult::CommandOk { affected, .. } => *affected,
8856 QueryResult::Rows { .. } => panic!("expected CommandOk, got Rows"),
8857 }
8858 }
8859
8860 #[test]
8861 fn create_table_registers_schema() {
8862 let mut e = Engine::new();
8863 e.execute("CREATE TABLE foo (a INT NOT NULL, b TEXT)")
8864 .unwrap();
8865 assert_eq!(e.catalog().table_count(), 1);
8866 let t = e.catalog().get("foo").unwrap();
8867 assert_eq!(t.schema().columns.len(), 2);
8868 assert_eq!(t.schema().columns[0].ty, DataType::Int);
8869 assert!(!t.schema().columns[0].nullable);
8870 assert_eq!(t.schema().columns[1].ty, DataType::Text);
8871 }
8872
8873 #[test]
8874 fn create_table_vector_default_is_f32_encoded() {
8875 let mut e = Engine::new();
8876 e.execute("CREATE TABLE t (v VECTOR(8))").unwrap();
8877 let t = e.catalog().get("t").unwrap();
8878 assert_eq!(
8879 t.schema().columns[0].ty,
8880 DataType::Vector {
8881 dim: 8,
8882 encoding: VecEncoding::F32,
8883 },
8884 );
8885 }
8886
8887 #[test]
8888 fn create_table_vector_using_sq8_succeeds() {
8889 let mut e = Engine::new();
8893 e.execute("CREATE TABLE t (v VECTOR(8) USING SQ8)").unwrap();
8894 let t = e.catalog().get("t").unwrap();
8895 assert_eq!(
8896 t.schema().columns[0].ty,
8897 DataType::Vector {
8898 dim: 8,
8899 encoding: VecEncoding::Sq8,
8900 },
8901 );
8902 }
8903
8904 #[test]
8905 fn insert_into_sq8_column_quantises_f32_payload() {
8906 let mut e = Engine::new();
8913 e.execute("CREATE TABLE t (v VECTOR(4) USING SQ8)").unwrap();
8914 e.execute("INSERT INTO t VALUES ([0.0, 0.25, 0.5, 1.0])")
8915 .unwrap();
8916 let t = e.catalog().get("t").unwrap();
8917 assert_eq!(t.rows().len(), 1);
8918 match &t.rows()[0].values[0] {
8919 Value::Sq8Vector(q) => {
8920 assert_eq!(q.bytes.len(), 4);
8921 assert!((q.min - 0.0).abs() < 1e-6);
8923 assert!((q.max - 1.0).abs() < 1e-6);
8924 }
8925 other => panic!("expected Sq8Vector cell, got {other:?}"),
8926 }
8927 }
8928
8929 #[test]
8930 fn create_table_vector_using_half_succeeds_and_insert_converts_to_f16() {
8931 let mut e = Engine::new();
8938 e.execute("CREATE TABLE t (v VECTOR(4) USING HALF)")
8939 .unwrap();
8940 e.execute("INSERT INTO t VALUES ([0.0, 0.25, 0.5, 1.0])")
8941 .unwrap();
8942 let t = e.catalog().get("t").unwrap();
8943 assert_eq!(t.rows().len(), 1);
8944 match &t.rows()[0].values[0] {
8945 Value::HalfVector(h) => {
8946 assert_eq!(h.dim(), 4);
8947 let back = h.to_f32_vec();
8948 let expected = alloc::vec![0.0_f32, 0.25, 0.5, 1.0];
8949 for (g, e) in back.iter().zip(expected.iter()) {
8950 assert!(
8951 (g - e).abs() < 1e-6,
8952 "{g} vs {e} should be exact on f16 grid"
8953 );
8954 }
8955 }
8956 other => panic!("expected HalfVector cell, got {other:?}"),
8957 }
8958 }
8959
8960 #[test]
8961 fn alter_index_rebuild_in_place_succeeds() {
8962 let mut e = Engine::new();
8967 e.execute("CREATE TABLE t (id INT NOT NULL, v VECTOR(3) NOT NULL)")
8968 .unwrap();
8969 for i in 0..8_i32 {
8970 #[allow(clippy::cast_precision_loss)]
8971 let base = (i as f32) * 0.1;
8972 e.execute(&alloc::format!(
8973 "INSERT INTO t VALUES ({i}, [{base}, {b1}, {b2}])",
8974 b1 = base + 0.01,
8975 b2 = base + 0.02,
8976 ))
8977 .unwrap();
8978 }
8979 e.execute("CREATE INDEX t_idx ON t USING hnsw (v)").unwrap();
8980 e.execute("ALTER INDEX t_idx REBUILD").unwrap();
8981 assert_eq!(
8983 e.catalog().get("t").unwrap().schema().columns[1].ty,
8984 DataType::Vector {
8985 dim: 3,
8986 encoding: VecEncoding::F32,
8987 },
8988 );
8989 }
8990
8991 #[test]
8992 fn alter_index_rebuild_with_encoding_switches_cell_type() {
8993 let mut e = Engine::new();
8998 e.execute("CREATE TABLE t (id INT NOT NULL, v VECTOR(4) NOT NULL)")
8999 .unwrap();
9000 e.execute("INSERT INTO t VALUES (1, [0.0, 0.25, 0.5, 1.0])")
9001 .unwrap();
9002 e.execute("CREATE INDEX t_idx ON t USING hnsw (v)").unwrap();
9003 e.execute("ALTER INDEX t_idx REBUILD WITH (encoding = SQ8)")
9004 .unwrap();
9005 let t = e.catalog().get("t").unwrap();
9006 assert_eq!(
9007 t.schema().columns[1].ty,
9008 DataType::Vector {
9009 dim: 4,
9010 encoding: VecEncoding::Sq8,
9011 },
9012 );
9013 assert!(matches!(t.rows()[0].values[1], Value::Sq8Vector(_)));
9014 }
9015
9016 #[test]
9017 fn alter_index_rebuild_unknown_index_errors() {
9018 let mut e = Engine::new();
9019 let err = e.execute("ALTER INDEX nope REBUILD").unwrap_err();
9020 assert!(
9021 matches!(
9022 &err,
9023 EngineError::Storage(StorageError::IndexNotFound { name }) if name == "nope"
9024 ),
9025 "got: {err}"
9026 );
9027 }
9028
9029 #[test]
9030 fn alter_index_rebuild_on_btree_index_errors() {
9031 let mut e = Engine::new();
9034 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9035 e.execute("INSERT INTO t VALUES (1)").unwrap();
9036 e.execute("CREATE INDEX t_idx ON t (id)").unwrap();
9037 let err = e.execute("ALTER INDEX t_idx REBUILD").unwrap_err();
9038 assert!(
9039 matches!(&err, EngineError::Storage(StorageError::Unsupported(_))),
9040 "got: {err}"
9041 );
9042 }
9043
9044 #[test]
9045 fn prepared_insert_substitutes_placeholders() {
9046 let mut e = Engine::new();
9052 e.execute("CREATE TABLE t (id INT NOT NULL, name TEXT NOT NULL)")
9053 .unwrap();
9054 let stmt = e.prepare("INSERT INTO t VALUES ($1, $2)").unwrap();
9055 for (id, name) in [(1, "alice"), (2, "bob"), (3, "carol")] {
9056 e.execute_prepared(
9057 stmt.clone(),
9058 &[Value::Int(id), Value::Text(name.into())],
9059 )
9060 .unwrap();
9061 }
9062 let rows_result = e.execute("SELECT id, name FROM t").unwrap();
9064 let QueryResult::Rows { rows, .. } = rows_result else {
9065 panic!("expected Rows")
9066 };
9067 assert_eq!(rows.len(), 3);
9068 }
9069
9070 #[test]
9071 fn prepared_select_with_placeholder_filters_rows() {
9072 let mut e = Engine::new();
9073 e.execute("CREATE TABLE t (id INT NOT NULL, v INT NOT NULL)")
9074 .unwrap();
9075 for i in 0..10_i32 {
9076 e.execute(&alloc::format!("INSERT INTO t VALUES ({i}, {})", i * 7))
9077 .unwrap();
9078 }
9079 let stmt = e
9080 .prepare("SELECT id FROM t WHERE v = $1")
9081 .unwrap();
9082 let QueryResult::Rows { rows, .. } = e
9083 .execute_prepared(stmt, &[Value::Int(35)])
9084 .unwrap()
9085 else {
9086 panic!("expected Rows")
9087 };
9088 assert_eq!(rows.len(), 1);
9090 assert_eq!(rows[0].values[0], Value::Int(5));
9091 }
9092
9093 #[test]
9094 fn prepared_too_few_params_errors() {
9095 let mut e = Engine::new();
9096 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9097 let stmt = e.prepare("INSERT INTO t VALUES ($1)").unwrap();
9098 let err = e.execute_prepared(stmt, &[]).unwrap_err();
9099 assert!(
9100 matches!(
9101 &err,
9102 EngineError::Eval(EvalError::PlaceholderOutOfRange { n: 1, bound: 0 })
9103 ),
9104 "got: {err}"
9105 );
9106 }
9107
9108 #[test]
9109 fn insert_into_half_column_dim_mismatch_errors() {
9110 let mut e = Engine::new();
9111 e.execute("CREATE TABLE t (v VECTOR(4) USING HALF)")
9112 .unwrap();
9113 let err = e.execute("INSERT INTO t VALUES ([1.0, 2.0])").unwrap_err();
9114 assert!(matches!(
9115 &err,
9116 EngineError::Storage(StorageError::TypeMismatch { .. })
9117 ));
9118 }
9119
9120 #[test]
9121 fn insert_into_sq8_column_dim_mismatch_errors() {
9122 let mut e = Engine::new();
9127 e.execute("CREATE TABLE t (v VECTOR(4) USING SQ8)").unwrap();
9128 let err = e.execute("INSERT INTO t VALUES ([1.0, 2.0])").unwrap_err();
9129 assert!(
9130 matches!(
9131 &err,
9132 EngineError::Storage(StorageError::TypeMismatch { .. })
9133 ),
9134 "got: {err}",
9135 );
9136 }
9137
9138 #[test]
9139 fn create_table_duplicate_errors() {
9140 let mut e = Engine::new();
9141 e.execute("CREATE TABLE foo (a INT)").unwrap();
9142 let err = e.execute("CREATE TABLE foo (a INT)").unwrap_err();
9143 assert!(matches!(
9144 err,
9145 EngineError::Storage(StorageError::DuplicateTable { ref name }) if name == "foo"
9146 ));
9147 }
9148
9149 #[test]
9150 fn insert_into_unknown_table_errors() {
9151 let mut e = Engine::new();
9152 let err = e.execute("INSERT INTO ghost VALUES (1)").unwrap_err();
9153 assert!(matches!(
9154 err,
9155 EngineError::Storage(StorageError::TableNotFound { ref name }) if name == "ghost"
9156 ));
9157 }
9158
9159 #[test]
9160 fn insert_happy_path_reports_one_affected() {
9161 let mut e = Engine::new();
9162 e.execute("CREATE TABLE foo (a INT NOT NULL)").unwrap();
9163 let r = e.execute("INSERT INTO foo VALUES (42)").unwrap();
9164 assert_eq!(unwrap_command_ok(&r), 1);
9165 assert_eq!(e.catalog().get("foo").unwrap().row_count(), 1);
9166 }
9167
9168 #[test]
9169 fn insert_arity_mismatch_propagates() {
9170 let mut e = Engine::new();
9171 e.execute("CREATE TABLE foo (a INT, b TEXT)").unwrap();
9172 let err = e.execute("INSERT INTO foo VALUES (1)").unwrap_err();
9173 assert!(matches!(
9174 err,
9175 EngineError::Storage(StorageError::ArityMismatch { .. })
9176 ));
9177 }
9178
9179 #[test]
9180 fn insert_negative_integer_via_unary_minus() {
9181 let mut e = Engine::new();
9182 e.execute("CREATE TABLE foo (a INT NOT NULL)").unwrap();
9183 e.execute("INSERT INTO foo VALUES (-7)").unwrap();
9184 let rows = e.catalog().get("foo").unwrap().rows();
9185 assert_eq!(rows[0].values[0], Value::Int(-7));
9186 }
9187
9188 #[test]
9189 fn insert_non_literal_expr_unsupported() {
9190 let mut e = Engine::new();
9191 e.execute("CREATE TABLE foo (a INT NOT NULL)").unwrap();
9192 let err = e.execute("INSERT INTO foo VALUES (1 + 2)").unwrap_err();
9193 assert!(matches!(err, EngineError::Unsupported(_)));
9194 }
9195
9196 #[test]
9197 fn select_star_returns_all_rows_in_insertion_order() {
9198 let mut e = Engine::new();
9199 e.execute("CREATE TABLE foo (a INT NOT NULL, b TEXT NOT NULL)")
9200 .unwrap();
9201 e.execute("INSERT INTO foo VALUES (1, 'one')").unwrap();
9202 e.execute("INSERT INTO foo VALUES (2, 'two')").unwrap();
9203 e.execute("INSERT INTO foo VALUES (3, 'three')").unwrap();
9204
9205 let r = e.execute("SELECT * FROM foo").unwrap();
9206 let QueryResult::Rows { columns, rows } = r else {
9207 panic!("expected Rows")
9208 };
9209 assert_eq!(columns.len(), 2);
9210 assert_eq!(columns[0].name, "a");
9211 assert_eq!(rows.len(), 3);
9212 assert_eq!(
9213 rows[1].values,
9214 vec![Value::Int(2), Value::Text("two".into())]
9215 );
9216 }
9217
9218 #[test]
9219 fn select_star_on_empty_table_returns_zero_rows() {
9220 let mut e = Engine::new();
9221 e.execute("CREATE TABLE foo (a INT)").unwrap();
9222 let r = e.execute("SELECT * FROM foo").unwrap();
9223 match r {
9224 QueryResult::Rows { rows, .. } => assert!(rows.is_empty()),
9225 QueryResult::CommandOk { .. } => panic!("expected Rows"),
9226 }
9227 }
9228
9229 fn make_three_row_users(e: &mut Engine) {
9232 e.execute("CREATE TABLE users (id INT NOT NULL, name TEXT NOT NULL, score INT)")
9233 .unwrap();
9234 e.execute("INSERT INTO users VALUES (1, 'alice', 90)")
9235 .unwrap();
9236 e.execute("INSERT INTO users VALUES (2, 'bob', NULL)")
9237 .unwrap();
9238 e.execute("INSERT INTO users VALUES (3, 'cara', 70)")
9239 .unwrap();
9240 }
9241
9242 fn unwrap_rows(r: QueryResult) -> (Vec<ColumnSchema>, Vec<Row>) {
9243 match r {
9244 QueryResult::Rows { columns, rows } => (columns, rows),
9245 QueryResult::CommandOk { .. } => panic!("expected Rows"),
9246 }
9247 }
9248
9249 #[test]
9250 fn where_filter_passes_only_true_rows() {
9251 let mut e = Engine::new();
9252 make_three_row_users(&mut e);
9253 let r = e.execute("SELECT * FROM users WHERE id > 1").unwrap();
9254 let (_, rows) = unwrap_rows(r);
9255 assert_eq!(rows.len(), 2);
9256 assert_eq!(rows[0].values[0], Value::Int(2));
9257 assert_eq!(rows[1].values[0], Value::Int(3));
9258 }
9259
9260 #[test]
9261 fn where_with_null_result_filters_out_row() {
9262 let mut e = Engine::new();
9263 make_three_row_users(&mut e);
9264 let r = e.execute("SELECT * FROM users WHERE score > 80").unwrap();
9266 let (_, rows) = unwrap_rows(r);
9267 assert_eq!(rows.len(), 1);
9268 assert_eq!(rows[0].values[1], Value::Text("alice".into()));
9269 }
9270
9271 #[test]
9272 fn projection_named_columns() {
9273 let mut e = Engine::new();
9274 make_three_row_users(&mut e);
9275 let r = e.execute("SELECT name, score FROM users").unwrap();
9276 let (cols, rows) = unwrap_rows(r);
9277 assert_eq!(cols.len(), 2);
9278 assert_eq!(cols[0].name, "name");
9279 assert_eq!(cols[1].name, "score");
9280 assert_eq!(rows.len(), 3);
9281 assert_eq!(
9282 rows[0].values,
9283 vec![Value::Text("alice".into()), Value::Int(90)]
9284 );
9285 }
9286
9287 #[test]
9288 fn projection_with_column_alias() {
9289 let mut e = Engine::new();
9290 make_three_row_users(&mut e);
9291 let r = e
9292 .execute("SELECT name AS who FROM users WHERE id = 1")
9293 .unwrap();
9294 let (cols, rows) = unwrap_rows(r);
9295 assert_eq!(cols[0].name, "who");
9296 assert_eq!(rows.len(), 1);
9297 assert_eq!(rows[0].values[0], Value::Text("alice".into()));
9298 }
9299
9300 #[test]
9301 fn qualified_column_with_table_alias_resolves() {
9302 let mut e = Engine::new();
9303 make_three_row_users(&mut e);
9304 let r = e
9305 .execute("SELECT u.id, u.name FROM users AS u WHERE u.id < 3")
9306 .unwrap();
9307 let (cols, rows) = unwrap_rows(r);
9308 assert_eq!(cols.len(), 2);
9309 assert_eq!(rows.len(), 2);
9310 }
9311
9312 #[test]
9313 fn qualified_column_with_wrong_alias_errors() {
9314 let mut e = Engine::new();
9315 make_three_row_users(&mut e);
9316 let err = e.execute("SELECT x.id FROM users AS u").unwrap_err();
9317 assert!(matches!(
9318 err,
9319 EngineError::Eval(EvalError::UnknownQualifier { ref qualifier }) if qualifier == "x"
9320 ));
9321 }
9322
9323 #[test]
9324 fn select_unknown_column_errors_in_projection() {
9325 let mut e = Engine::new();
9326 make_three_row_users(&mut e);
9327 let err = e.execute("SELECT ghost FROM users").unwrap_err();
9328 assert!(matches!(
9329 err,
9330 EngineError::Eval(EvalError::ColumnNotFound { ref name }) if name == "ghost"
9331 ));
9332 }
9333
9334 #[test]
9335 fn where_unknown_column_errors() {
9336 let mut e = Engine::new();
9337 make_three_row_users(&mut e);
9338 let err = e
9339 .execute("SELECT * FROM users WHERE ghost = 1")
9340 .unwrap_err();
9341 assert!(matches!(
9342 err,
9343 EngineError::Eval(EvalError::ColumnNotFound { .. })
9344 ));
9345 }
9346
9347 #[test]
9348 fn expression_projection_evaluates_and_renders() {
9349 let mut e = Engine::new();
9352 e.execute("CREATE TABLE t (a INT NOT NULL)").unwrap();
9353 e.execute("INSERT INTO t VALUES (3)").unwrap();
9354 let (_, rows) = unwrap_rows(e.execute("SELECT 1 + 2 FROM t").unwrap());
9355 assert_eq!(rows.len(), 1);
9356 assert_eq!(rows[0].values[0], Value::Int(3));
9359 }
9360
9361 #[test]
9362 fn select_unknown_table_errors() {
9363 let mut e = Engine::new();
9364 let err = e.execute("SELECT * FROM ghost").unwrap_err();
9365 assert!(matches!(
9366 err,
9367 EngineError::Storage(StorageError::TableNotFound { .. })
9368 ));
9369 }
9370
9371 #[test]
9372 fn invalid_sql_returns_parse_error() {
9373 let mut e = Engine::new();
9376 let err = e.execute("THIS_IS_NOT_A_KEYWORD foo bar baz").unwrap_err();
9377 assert!(matches!(err, EngineError::Parse(_)));
9378 }
9379
9380 #[test]
9383 fn create_index_registers_on_table() {
9384 let mut e = Engine::new();
9385 make_three_row_users(&mut e);
9386 e.execute("CREATE INDEX by_name ON users (name)").unwrap();
9387 let t = e.catalog().get("users").unwrap();
9388 assert_eq!(t.indices().len(), 1);
9389 assert_eq!(t.indices()[0].name, "by_name");
9390 }
9391
9392 #[test]
9393 fn create_index_on_unknown_table_errors() {
9394 let mut e = Engine::new();
9395 let err = e.execute("CREATE INDEX i ON ghost (a)").unwrap_err();
9396 assert!(matches!(
9397 err,
9398 EngineError::Storage(StorageError::TableNotFound { .. })
9399 ));
9400 }
9401
9402 #[test]
9403 fn create_index_on_unknown_column_errors() {
9404 let mut e = Engine::new();
9405 make_three_row_users(&mut e);
9406 let err = e.execute("CREATE INDEX i ON users (ghost)").unwrap_err();
9407 assert!(matches!(
9408 err,
9409 EngineError::Storage(StorageError::ColumnNotFound { .. })
9410 ));
9411 }
9412
9413 #[test]
9414 fn select_eq_uses_index_returns_same_rows_as_scan() {
9415 let mut without = Engine::new();
9419 make_three_row_users(&mut without);
9420 let mut with = Engine::new();
9421 make_three_row_users(&mut with);
9422 with.execute("CREATE INDEX by_id ON users (id)").unwrap();
9423
9424 let q = "SELECT * FROM users WHERE id = 2";
9425 let (_, no_idx_rows) = unwrap_rows(without.execute(q).unwrap());
9426 let (_, idx_rows) = unwrap_rows(with.execute(q).unwrap());
9427 assert_eq!(no_idx_rows, idx_rows);
9428 assert_eq!(idx_rows.len(), 1);
9429 }
9430
9431 #[test]
9432 fn select_eq_with_no_matching_index_value_returns_empty() {
9433 let mut e = Engine::new();
9434 make_three_row_users(&mut e);
9435 e.execute("CREATE INDEX by_id ON users (id)").unwrap();
9436 let (_, rows) = unwrap_rows(e.execute("SELECT * FROM users WHERE id = 999").unwrap());
9437 assert_eq!(rows.len(), 0);
9438 }
9439
9440 #[test]
9443 fn begin_sets_in_transaction_flag() {
9444 let mut e = Engine::new();
9445 assert!(!e.in_transaction());
9446 e.execute("BEGIN").unwrap();
9447 assert!(e.in_transaction());
9448 }
9449
9450 #[test]
9451 fn double_begin_errors() {
9452 let mut e = Engine::new();
9453 e.execute("BEGIN").unwrap();
9454 let err = e.execute("BEGIN").unwrap_err();
9455 assert_eq!(err, EngineError::TransactionAlreadyOpen);
9456 }
9457
9458 #[test]
9459 fn commit_without_begin_errors() {
9460 let mut e = Engine::new();
9461 let err = e.execute("COMMIT").unwrap_err();
9462 assert_eq!(err, EngineError::NoActiveTransaction);
9463 }
9464
9465 #[test]
9466 fn rollback_without_begin_errors() {
9467 let mut e = Engine::new();
9468 let err = e.execute("ROLLBACK").unwrap_err();
9469 assert_eq!(err, EngineError::NoActiveTransaction);
9470 }
9471
9472 #[test]
9473 fn commit_applies_shadow_to_committed_catalog() {
9474 let mut e = Engine::new();
9475 e.execute("CREATE TABLE t (v INT NOT NULL)").unwrap();
9476 e.execute("BEGIN").unwrap();
9477 e.execute("INSERT INTO t VALUES (1)").unwrap();
9478 e.execute("INSERT INTO t VALUES (2)").unwrap();
9479 e.execute("COMMIT").unwrap();
9480 assert!(!e.in_transaction());
9481 assert_eq!(e.catalog().get("t").unwrap().row_count(), 2);
9482 }
9483
9484 #[test]
9485 fn rollback_discards_shadow() {
9486 let mut e = Engine::new();
9487 e.execute("CREATE TABLE t (v INT NOT NULL)").unwrap();
9488 e.execute("BEGIN").unwrap();
9489 e.execute("INSERT INTO t VALUES (1)").unwrap();
9490 e.execute("INSERT INTO t VALUES (2)").unwrap();
9491 e.execute("ROLLBACK").unwrap();
9492 assert!(!e.in_transaction());
9493 assert_eq!(e.catalog().get("t").unwrap().row_count(), 0);
9494 }
9495
9496 #[test]
9497 fn select_during_tx_sees_uncommitted_writes_own_session() {
9498 let mut e = Engine::new();
9501 e.execute("CREATE TABLE t (v INT NOT NULL)").unwrap();
9502 e.execute("BEGIN").unwrap();
9503 e.execute("INSERT INTO t VALUES (42)").unwrap();
9504 let (_, rows) = unwrap_rows(e.execute("SELECT * FROM t").unwrap());
9505 assert_eq!(rows.len(), 1);
9506 assert_eq!(rows[0].values[0], Value::Int(42));
9507 }
9508
9509 #[test]
9510 fn snapshot_with_no_users_is_bare_catalog_format() {
9511 let mut e = Engine::new();
9512 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9513 let bytes = e.snapshot();
9514 assert_eq!(
9515 &bytes[..8],
9516 b"SPGDB001",
9517 "must be the bare v3.x catalog magic"
9518 );
9519 let e2 = Engine::restore_envelope(&bytes).unwrap();
9520 assert!(e2.users().is_empty());
9521 assert_eq!(e2.catalog().table_count(), 1);
9522 }
9523
9524 #[test]
9525 fn snapshot_with_users_round_trips_both_via_envelope() {
9526 let mut e = Engine::new();
9527 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9528 e.create_user("alice", "pw1", Role::Admin, [9; 16]).unwrap();
9529 e.create_user("bob", "pw2", Role::ReadOnly, [5; 16])
9530 .unwrap();
9531 let bytes = e.snapshot();
9532 assert_eq!(&bytes[..8], b"SPGENV01", "must be the v4.1 envelope magic");
9533 let e2 = Engine::restore_envelope(&bytes).unwrap();
9534 assert_eq!(e2.users().len(), 2);
9535 assert_eq!(e2.verify_user("alice", "pw1"), Some(Role::Admin));
9536 assert_eq!(e2.verify_user("bob", "pw2"), Some(Role::ReadOnly));
9537 assert_eq!(e2.verify_user("alice", "wrong"), None);
9538 assert_eq!(e2.catalog().table_count(), 1);
9539 }
9540
9541 #[test]
9542 fn ddl_inside_tx_also_rolled_back() {
9543 let mut e = Engine::new();
9544 e.execute("BEGIN").unwrap();
9545 e.execute("CREATE TABLE t (v INT)").unwrap();
9546 e.execute("SELECT * FROM t").unwrap();
9548 e.execute("ROLLBACK").unwrap();
9549 let err = e.execute("SELECT * FROM t").unwrap_err();
9551 assert!(matches!(
9552 err,
9553 EngineError::Storage(StorageError::TableNotFound { .. })
9554 ));
9555 }
9556
9557 #[test]
9560 fn create_publication_lands_in_catalog() {
9561 let mut e = Engine::new();
9562 assert!(e.publications().is_empty());
9563 e.execute("CREATE PUBLICATION pub_a").unwrap();
9564 assert_eq!(e.publications().len(), 1);
9565 assert!(e.publications().contains("pub_a"));
9566 }
9567
9568 #[test]
9569 fn create_publication_duplicate_errors() {
9570 let mut e = Engine::new();
9571 e.execute("CREATE PUBLICATION pub_a").unwrap();
9572 let err = e.execute("CREATE PUBLICATION pub_a").unwrap_err();
9573 assert!(
9574 alloc::format!("{err:?}").contains("DuplicateName"),
9575 "got {err:?}"
9576 );
9577 }
9578
9579 #[test]
9580 fn drop_publication_silent_when_absent() {
9581 let mut e = Engine::new();
9582 let r = e.execute("DROP PUBLICATION nope").unwrap();
9585 match r {
9586 QueryResult::CommandOk { affected, .. } => assert_eq!(affected, 0),
9587 other => panic!("expected CommandOk, got {other:?}"),
9588 }
9589 }
9590
9591 #[test]
9592 fn drop_publication_present_reports_one_affected() {
9593 let mut e = Engine::new();
9594 e.execute("CREATE PUBLICATION pub_a").unwrap();
9595 let r = e.execute("DROP PUBLICATION pub_a").unwrap();
9596 match r {
9597 QueryResult::CommandOk {
9598 affected,
9599 modified_catalog,
9600 } => {
9601 assert_eq!(affected, 1);
9602 assert!(modified_catalog);
9603 }
9604 other => panic!("expected CommandOk, got {other:?}"),
9605 }
9606 assert!(e.publications().is_empty());
9607 }
9608
9609 #[test]
9610 fn publications_persist_across_snapshot_restore() {
9611 let mut e = Engine::new();
9616 e.execute("CREATE PUBLICATION pub_a").unwrap();
9617 e.execute("CREATE PUBLICATION pub_b FOR ALL TABLES").unwrap();
9618 let snap = e.snapshot();
9619 let e2 = Engine::restore_envelope(&snap).unwrap();
9620 assert_eq!(e2.publications().len(), 2);
9621 assert!(e2.publications().contains("pub_a"));
9622 assert!(e2.publications().contains("pub_b"));
9623 }
9624
9625 #[test]
9626 fn create_publication_allowed_inside_transaction() {
9627 let mut e = Engine::new();
9631 e.execute("BEGIN").unwrap();
9632 e.execute("CREATE PUBLICATION pub_a").unwrap();
9633 e.execute("COMMIT").unwrap();
9634 assert!(e.publications().contains("pub_a"));
9635 }
9636
9637 #[test]
9640 fn create_publication_for_table_list_lands_with_scope() {
9641 let mut e = Engine::new();
9642 e.execute("CREATE TABLE t1 (id INT NOT NULL)").unwrap();
9643 e.execute("CREATE TABLE t2 (id INT NOT NULL)").unwrap();
9644 e.execute("CREATE PUBLICATION pub_a FOR TABLE t1, t2")
9645 .unwrap();
9646 let scope = e.publications().get("pub_a").cloned();
9647 let Some(spg_sql::ast::PublicationScope::ForTables(ts)) = scope else {
9648 panic!("expected ForTables scope, got {scope:?}")
9649 };
9650 assert_eq!(ts, alloc::vec!["t1".to_string(), "t2".to_string()]);
9651 }
9652
9653 #[test]
9654 fn create_publication_all_tables_except_lands_with_scope() {
9655 let mut e = Engine::new();
9656 e.execute("CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t3")
9657 .unwrap();
9658 let scope = e.publications().get("pub_a").cloned();
9659 let Some(spg_sql::ast::PublicationScope::AllTablesExcept(ts)) = scope else {
9660 panic!("expected AllTablesExcept scope, got {scope:?}")
9661 };
9662 assert_eq!(ts, alloc::vec!["t3".to_string()]);
9663 }
9664
9665 #[test]
9666 fn show_publications_empty_returns_zero_rows() {
9667 let e = Engine::new();
9668 let r = e.execute_readonly("SHOW PUBLICATIONS").unwrap();
9669 let QueryResult::Rows { rows, columns } = r else {
9670 panic!()
9671 };
9672 assert!(rows.is_empty());
9673 assert_eq!(columns.len(), 3);
9674 assert_eq!(columns[0].name, "name");
9675 assert_eq!(columns[1].name, "scope");
9676 assert_eq!(columns[2].name, "table_count");
9677 }
9678
9679 #[test]
9680 fn show_publications_returns_one_row_per_publication_ordered_by_name() {
9681 let mut e = Engine::new();
9682 e.execute("CREATE PUBLICATION z_pub").unwrap();
9683 e.execute("CREATE PUBLICATION a_pub FOR TABLE t1, t2")
9684 .unwrap();
9685 e.execute("CREATE PUBLICATION m_pub FOR ALL TABLES EXCEPT bad")
9686 .unwrap();
9687 let r = e.execute_readonly("SHOW PUBLICATIONS").unwrap();
9688 let QueryResult::Rows { rows, .. } = r else {
9689 panic!()
9690 };
9691 assert_eq!(rows.len(), 3);
9692 let names: Vec<&str> = rows
9694 .iter()
9695 .map(|r| {
9696 if let Value::Text(s) = &r.values[0] {
9697 s.as_str()
9698 } else {
9699 panic!()
9700 }
9701 })
9702 .collect();
9703 assert_eq!(names, alloc::vec!["a_pub", "m_pub", "z_pub"]);
9704 match &rows[0].values[1] {
9706 Value::Text(s) => assert_eq!(s, "FOR TABLE t1, t2"),
9707 other => panic!("expected Text, got {other:?}"),
9708 }
9709 assert_eq!(rows[0].values[2], Value::Int(2));
9710 match &rows[1].values[1] {
9712 Value::Text(s) => assert_eq!(s, "FOR ALL TABLES EXCEPT bad"),
9713 other => panic!("expected Text, got {other:?}"),
9714 }
9715 assert_eq!(rows[1].values[2], Value::Int(1));
9716 match &rows[2].values[1] {
9718 Value::Text(s) => assert_eq!(s, "FOR ALL TABLES"),
9719 other => panic!("expected Text, got {other:?}"),
9720 }
9721 assert_eq!(rows[2].values[2], Value::Null);
9722 }
9723
9724 #[test]
9725 fn for_list_scopes_persist_across_snapshot() {
9726 let mut e = Engine::new();
9729 e.execute("CREATE PUBLICATION p1 FOR TABLE t1, t2").unwrap();
9730 e.execute("CREATE PUBLICATION p2 FOR ALL TABLES EXCEPT bad, worse")
9731 .unwrap();
9732 let snap = e.snapshot();
9733 let e2 = Engine::restore_envelope(&snap).unwrap();
9734 assert_eq!(e2.publications().len(), 2);
9735 let p1 = e2.publications().get("p1").cloned();
9736 let Some(spg_sql::ast::PublicationScope::ForTables(ts)) = p1 else {
9737 panic!("p1 scope lost: {p1:?}")
9738 };
9739 assert_eq!(ts, alloc::vec!["t1".to_string(), "t2".to_string()]);
9740 let p2 = e2.publications().get("p2").cloned();
9741 let Some(spg_sql::ast::PublicationScope::AllTablesExcept(ts)) = p2 else {
9742 panic!("p2 scope lost: {p2:?}")
9743 };
9744 assert_eq!(ts, alloc::vec!["bad".to_string(), "worse".to_string()]);
9745 }
9746
9747 #[test]
9750 fn create_subscription_lands_in_catalog_with_defaults() {
9751 let mut e = Engine::new();
9752 e.execute(
9753 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
9754 )
9755 .unwrap();
9756 let s = e.subscriptions().get("sub_a").cloned().expect("present");
9757 assert_eq!(s.conn_str, "host=127.0.0.1 port=20002");
9758 assert_eq!(s.publications, alloc::vec!["pub_a".to_string()]);
9759 assert!(s.enabled);
9760 assert_eq!(s.last_received_pos, 0);
9761 }
9762
9763 #[test]
9764 fn create_subscription_duplicate_name_errors() {
9765 let mut e = Engine::new();
9766 e.execute("CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p")
9767 .unwrap();
9768 let err = e
9769 .execute("CREATE SUBSCRIPTION s CONNECTION 'host=y' PUBLICATION p")
9770 .unwrap_err();
9771 assert!(
9772 alloc::format!("{err:?}").contains("DuplicateName"),
9773 "got {err:?}"
9774 );
9775 }
9776
9777 #[test]
9778 fn drop_subscription_silent_when_absent() {
9779 let mut e = Engine::new();
9780 let r = e.execute("DROP SUBSCRIPTION never").unwrap();
9781 match r {
9782 QueryResult::CommandOk { affected, .. } => assert_eq!(affected, 0),
9783 other => panic!("expected CommandOk, got {other:?}"),
9784 }
9785 }
9786
9787 #[test]
9788 fn subscription_advance_updates_last_pos_monotone() {
9789 let mut e = Engine::new();
9790 e.execute("CREATE SUBSCRIPTION s CONNECTION 'h=x' PUBLICATION p")
9791 .unwrap();
9792 assert!(e.subscription_advance("s", 100));
9793 assert_eq!(e.subscriptions().get("s").unwrap().last_received_pos, 100);
9794 assert!(e.subscription_advance("s", 50)); assert_eq!(e.subscriptions().get("s").unwrap().last_received_pos, 100);
9796 assert!(e.subscription_advance("s", 200));
9797 assert_eq!(e.subscriptions().get("s").unwrap().last_received_pos, 200);
9798 assert!(!e.subscription_advance("missing", 1));
9799 }
9800
9801 #[test]
9802 fn show_subscriptions_returns_rows_ordered_by_name() {
9803 let mut e = Engine::new();
9804 e.execute("CREATE SUBSCRIPTION z_sub CONNECTION 'h=x' PUBLICATION p1, p2")
9805 .unwrap();
9806 e.execute("CREATE SUBSCRIPTION a_sub CONNECTION 'h=y' PUBLICATION p3")
9807 .unwrap();
9808 let r = e.execute_readonly("SHOW SUBSCRIPTIONS").unwrap();
9809 let QueryResult::Rows { rows, columns } = r else {
9810 panic!()
9811 };
9812 assert_eq!(rows.len(), 2);
9813 assert_eq!(columns.len(), 5);
9814 assert_eq!(columns[0].name, "name");
9815 assert_eq!(columns[4].name, "last_received_pos");
9816 let names: Vec<&str> = rows
9818 .iter()
9819 .map(|r| {
9820 if let Value::Text(s) = &r.values[0] {
9821 s.as_str()
9822 } else {
9823 panic!()
9824 }
9825 })
9826 .collect();
9827 assert_eq!(names, alloc::vec!["a_sub", "z_sub"]);
9828 assert_eq!(rows[0].values[1], Value::Text("h=y".to_string()));
9830 assert_eq!(rows[0].values[2], Value::Text("p3".to_string()));
9831 assert_eq!(rows[0].values[3], Value::Bool(true));
9832 assert_eq!(rows[0].values[4], Value::BigInt(0));
9833 assert_eq!(rows[1].values[2], Value::Text("p1, p2".to_string()));
9835 }
9836
9837 #[test]
9838 fn subscriptions_persist_across_snapshot_envelope_v4() {
9839 let mut e = Engine::new();
9840 e.execute("CREATE SUBSCRIPTION s1 CONNECTION 'h=A' PUBLICATION p1, p2")
9841 .unwrap();
9842 e.execute("CREATE SUBSCRIPTION s2 CONNECTION 'h=B' PUBLICATION p3")
9843 .unwrap();
9844 e.subscription_advance("s2", 42);
9845 let snap = e.snapshot();
9846 let e2 = Engine::restore_envelope(&snap).unwrap();
9847 assert_eq!(e2.subscriptions().len(), 2);
9848 let s1 = e2.subscriptions().get("s1").unwrap();
9849 assert_eq!(s1.conn_str, "h=A");
9850 assert_eq!(s1.publications, alloc::vec!["p1".to_string(), "p2".to_string()]);
9851 assert_eq!(s1.last_received_pos, 0);
9852 let s2 = e2.subscriptions().get("s2").unwrap();
9853 assert_eq!(s2.last_received_pos, 42);
9854 }
9855
9856 #[test]
9857 fn v3_envelope_loads_with_empty_subscriptions() {
9858 let mut e = Engine::new();
9862 e.execute("CREATE PUBLICATION pub_legacy").unwrap();
9863 let catalog = e.catalog.serialize();
9864 let users = crate::users::serialize_users(&e.users);
9865 let pubs = e.publications.serialize();
9866 let mut buf = Vec::new();
9867 buf.extend_from_slice(b"SPGENV01");
9868 buf.push(3u8); buf.extend_from_slice(&u32::try_from(catalog.len()).unwrap().to_le_bytes());
9870 buf.extend_from_slice(&catalog);
9871 buf.extend_from_slice(&u32::try_from(users.len()).unwrap().to_le_bytes());
9872 buf.extend_from_slice(&users);
9873 buf.extend_from_slice(&u32::try_from(pubs.len()).unwrap().to_le_bytes());
9874 buf.extend_from_slice(&pubs);
9875 let crc = spg_crypto::crc32::crc32(&buf);
9876 buf.extend_from_slice(&crc.to_le_bytes());
9877
9878 let e2 = Engine::restore_envelope(&buf).expect("v3 envelope restores under v4 reader");
9879 assert!(e2.subscriptions().is_empty());
9880 assert!(e2.publications().contains("pub_legacy"));
9881 }
9882
9883 #[test]
9884 fn create_subscription_allowed_inside_transaction() {
9885 let mut e = Engine::new();
9886 e.execute("BEGIN").unwrap();
9887 e.execute("CREATE SUBSCRIPTION s CONNECTION 'h=x' PUBLICATION p")
9888 .unwrap();
9889 e.execute("COMMIT").unwrap();
9890 assert!(e.subscriptions().contains("s"));
9891 }
9892
9893 #[test]
9894 #[test]
9897 fn analyze_populates_histogram_bounds() {
9898 let mut e = Engine::new();
9899 e.execute("CREATE TABLE t (id INT NOT NULL, name TEXT)").unwrap();
9900 for i in 0..50 {
9901 e.execute(&alloc::format!(
9902 "INSERT INTO t VALUES ({i}, 'name{i}')"
9903 ))
9904 .unwrap();
9905 }
9906 e.execute("ANALYZE t").unwrap();
9907 let stats = e.statistics();
9908 let id_stats = stats.get("t", "id").unwrap();
9909 assert!(id_stats.histogram_bounds.len() >= 2);
9910 assert_eq!(id_stats.histogram_bounds.first().unwrap(), "0");
9911 assert_eq!(id_stats.histogram_bounds.last().unwrap(), "49");
9912 assert!((id_stats.null_frac - 0.0).abs() < 1e-6);
9913 assert_eq!(id_stats.n_distinct, 50);
9914 }
9915
9916 #[test]
9917 fn reanalyze_overwrites_prior_stats() {
9918 let mut e = Engine::new();
9919 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9920 for i in 0..10 {
9921 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9922 }
9923 e.execute("ANALYZE t").unwrap();
9924 let n1 = e.statistics().get("t", "id").unwrap().n_distinct;
9925 assert_eq!(n1, 10);
9926 for i in 10..30 {
9927 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9928 }
9929 e.execute("ANALYZE t").unwrap();
9930 let n2 = e.statistics().get("t", "id").unwrap().n_distinct;
9931 assert_eq!(n2, 30);
9932 }
9933
9934 #[test]
9935 fn analyze_unknown_table_errors() {
9936 let mut e = Engine::new();
9937 let err = e.execute("ANALYZE nonexistent").unwrap_err();
9938 assert!(matches!(err, EngineError::Storage(StorageError::TableNotFound { .. })));
9939 }
9940
9941 #[test]
9942 fn bare_analyze_covers_all_user_tables() {
9943 let mut e = Engine::new();
9944 e.execute("CREATE TABLE t1 (id INT NOT NULL)").unwrap();
9945 e.execute("CREATE TABLE t2 (name TEXT NOT NULL)").unwrap();
9946 e.execute("INSERT INTO t1 VALUES (1)").unwrap();
9947 e.execute("INSERT INTO t2 VALUES ('alice')").unwrap();
9948 let r = e.execute("ANALYZE").unwrap();
9949 match r {
9950 QueryResult::CommandOk { affected, modified_catalog } => {
9951 assert_eq!(affected, 2);
9952 assert!(modified_catalog);
9953 }
9954 other => panic!("expected CommandOk, got {other:?}"),
9955 }
9956 assert!(e.statistics().get("t1", "id").is_some());
9957 assert!(e.statistics().get("t2", "name").is_some());
9958 }
9959
9960 #[test]
9961 fn select_from_spg_statistic_returns_rows_per_column() {
9962 let mut e = Engine::new();
9963 e.execute("CREATE TABLE t (id INT NOT NULL, label TEXT)")
9964 .unwrap();
9965 e.execute("INSERT INTO t VALUES (1, 'a')").unwrap();
9966 e.execute("INSERT INTO t VALUES (2, 'b')").unwrap();
9967 e.execute("ANALYZE t").unwrap();
9968 let r = e.execute_readonly("SELECT * FROM spg_statistic").unwrap();
9969 let QueryResult::Rows { rows, columns } = r else {
9970 panic!()
9971 };
9972 assert_eq!(columns.len(), 6);
9974 assert_eq!(columns[0].name, "table_name");
9975 assert_eq!(columns[4].name, "histogram_bounds");
9976 assert_eq!(columns[5].name, "cold_row_count");
9977 assert_eq!(rows.len(), 2, "one row per column of t");
9978 match (&rows[0].values[0], &rows[0].values[1]) {
9980 (Value::Text(t), Value::Text(c)) => {
9981 assert_eq!(t, "t");
9982 assert_eq!(c, "id");
9984 }
9985 _ => panic!(),
9986 }
9987 }
9988
9989 #[test]
9990 fn analyze_skips_vector_columns() {
9991 let mut e = Engine::new();
9994 e.execute("CREATE TABLE t (id INT NOT NULL, v VECTOR(3) NOT NULL)")
9995 .unwrap();
9996 e.execute("INSERT INTO t VALUES (1, [1, 2, 3])").unwrap();
9997 e.execute("ANALYZE t").unwrap();
9998 assert!(e.statistics().get("t", "id").is_some());
9999 assert!(e.statistics().get("t", "v").is_none());
10000 }
10001
10002 #[test]
10003 fn statistics_persist_across_envelope_v5_round_trip() {
10004 let mut e = Engine::new();
10005 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
10006 for i in 0..20 {
10007 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
10008 }
10009 e.execute("ANALYZE").unwrap();
10010 let snap = e.snapshot();
10011 let e2 = Engine::restore_envelope(&snap).unwrap();
10012 let s = e2.statistics().get("t", "id").unwrap();
10013 assert_eq!(s.n_distinct, 20);
10014 }
10015
10016 #[test]
10019 fn auto_analyze_threshold_fires_after_10pct_of_min_rows_on_small_table() {
10020 let mut e = Engine::new();
10024 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
10025 for i in 0..9 {
10026 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
10027 }
10028 assert!(e.tables_needing_analyze().is_empty(), "9 < threshold");
10029 e.execute("INSERT INTO t VALUES (9)").unwrap();
10030 let needs = e.tables_needing_analyze();
10031 assert_eq!(needs, alloc::vec!["t".to_string()]);
10032 }
10033
10034 #[test]
10035 fn auto_analyze_threshold_uses_10pct_of_row_count_for_large_tables() {
10036 let mut e = Engine::new();
10042 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
10043 for i in 0..1000 {
10044 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
10045 }
10046 e.execute("ANALYZE t").unwrap();
10047 assert!(e.tables_needing_analyze().is_empty(), "fresh ANALYZE");
10048 for i in 1000..1050 {
10049 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
10050 }
10051 assert!(
10052 e.tables_needing_analyze().is_empty(),
10053 "50 inserts < threshold of ~105"
10054 );
10055 for i in 1050..1200 {
10056 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
10057 }
10058 assert_eq!(
10059 e.tables_needing_analyze(),
10060 alloc::vec!["t".to_string()],
10061 "200 inserts > 0.1 × 1200 threshold"
10062 );
10063 }
10064
10065 #[test]
10066 fn auto_analyze_threshold_resets_after_analyze() {
10067 let mut e = Engine::new();
10068 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
10069 for i in 0..200 {
10070 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
10071 }
10072 assert!(!e.tables_needing_analyze().is_empty());
10073 e.execute("ANALYZE").unwrap();
10074 assert!(
10075 e.tables_needing_analyze().is_empty(),
10076 "ANALYZE must reset the counter"
10077 );
10078 }
10079
10080 #[test]
10081 fn auto_analyze_threshold_tracks_updates_and_deletes() {
10082 let mut e = Engine::new();
10083 e.execute("CREATE TABLE t (id INT NOT NULL, label TEXT)").unwrap();
10084 for i in 0..50 {
10085 e.execute(&alloc::format!("INSERT INTO t VALUES ({i}, 'x')"))
10086 .unwrap();
10087 }
10088 e.execute("ANALYZE t").unwrap();
10089 e.execute("UPDATE t SET label = 'y' WHERE id < 20").unwrap();
10092 e.execute("DELETE FROM t WHERE id >= 45").unwrap();
10093 assert_eq!(
10094 e.tables_needing_analyze(),
10095 alloc::vec!["t".to_string()]
10096 );
10097 }
10098
10099 #[test]
10100 fn v4_envelope_loads_with_empty_statistics() {
10101 let mut e = Engine::new();
10105 e.create_user("alice", "secret", crate::users::Role::ReadOnly, [0u8; 16])
10106 .unwrap();
10107 let catalog = e.catalog.serialize();
10108 let users = crate::users::serialize_users(&e.users);
10109 let pubs = e.publications.serialize();
10110 let subs = e.subscriptions.serialize();
10111 let mut buf = Vec::new();
10112 buf.extend_from_slice(b"SPGENV01");
10113 buf.push(4u8);
10114 buf.extend_from_slice(&u32::try_from(catalog.len()).unwrap().to_le_bytes());
10115 buf.extend_from_slice(&catalog);
10116 buf.extend_from_slice(&u32::try_from(users.len()).unwrap().to_le_bytes());
10117 buf.extend_from_slice(&users);
10118 buf.extend_from_slice(&u32::try_from(pubs.len()).unwrap().to_le_bytes());
10119 buf.extend_from_slice(&pubs);
10120 buf.extend_from_slice(&u32::try_from(subs.len()).unwrap().to_le_bytes());
10121 buf.extend_from_slice(&subs);
10122 let crc = spg_crypto::crc32::crc32(&buf);
10123 buf.extend_from_slice(&crc.to_le_bytes());
10124 let e2 = Engine::restore_envelope(&buf).expect("v4 envelope restores");
10125 assert!(e2.statistics().is_empty());
10126 }
10127
10128 #[test]
10129 fn v1_v2_envelope_loads_with_empty_publications() {
10130 let mut e = Engine::new();
10137 e.create_user(
10140 "alice",
10141 "secret",
10142 crate::users::Role::ReadOnly,
10143 [0u8; 16],
10144 )
10145 .unwrap();
10146
10147 let catalog = e.catalog.serialize();
10149 let users = crate::users::serialize_users(&e.users);
10150 let mut buf = Vec::new();
10151 buf.extend_from_slice(b"SPGENV01");
10152 buf.push(2u8); buf.extend_from_slice(
10154 &u32::try_from(catalog.len()).unwrap().to_le_bytes(),
10155 );
10156 buf.extend_from_slice(&catalog);
10157 buf.extend_from_slice(
10158 &u32::try_from(users.len()).unwrap().to_le_bytes(),
10159 );
10160 buf.extend_from_slice(&users);
10161 let crc = spg_crypto::crc32::crc32(&buf);
10162 buf.extend_from_slice(&crc.to_le_bytes());
10163
10164 let e2 = Engine::restore_envelope(&buf).expect("v2 envelope restores");
10165 assert!(e2.publications().is_empty());
10166 }
10167}