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 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
3030 .columns
3031 .into_iter()
3032 .map(column_def_to_schema)
3033 .collect::<Result<Vec<_>, _>>()?;
3034 let mut fks: Vec<spg_storage::ForeignKeyConstraint> =
3041 Vec::with_capacity(stmt.foreign_keys.len());
3042 for fk in stmt.foreign_keys {
3043 fks.push(resolve_foreign_key(
3044 &table_name,
3045 &cols,
3046 fk,
3047 self.active_catalog(),
3048 )?);
3049 }
3050 let mut schema = TableSchema::new(table_name.clone(), cols);
3051 schema.foreign_keys = fks;
3052 self.active_catalog_mut().create_table(schema)?;
3053 if !pk_columns.is_empty() {
3058 let table = self
3059 .active_catalog_mut()
3060 .get_mut(&table_name)
3061 .expect("just created");
3062 for (i, col_name) in pk_columns.iter().enumerate() {
3063 let idx_name = if pk_columns.len() == 1 {
3064 alloc::format!("{table_name}_pkey")
3065 } else {
3066 alloc::format!("{table_name}_pkey_{i}")
3067 };
3068 if let Err(e) = table.add_index(idx_name, col_name) {
3069 return Err(EngineError::Storage(e));
3070 }
3071 }
3072 }
3073 Ok(QueryResult::CommandOk {
3074 affected: 0,
3075 modified_catalog: !self.in_transaction(),
3076 })
3077 }
3078
3079 fn exec_insert(&mut self, stmt: InsertStatement) -> Result<QueryResult, EngineError> {
3080 let table = self
3081 .active_catalog_mut()
3082 .get_mut(&stmt.table)
3083 .ok_or_else(|| {
3084 EngineError::Storage(StorageError::TableNotFound {
3085 name: stmt.table.clone(),
3086 })
3087 })?;
3088 let column_meta: Vec<ColumnSchema> = table.schema().columns.clone();
3094 let schema_cols_len = column_meta.len();
3095 let tuple_pos: Option<Vec<Option<usize>>> = match &stmt.columns {
3099 None => None, Some(cols) => {
3101 let mut map = alloc::vec![None; schema_cols_len];
3102 for (j, name) in cols.iter().enumerate() {
3103 let idx = column_meta
3104 .iter()
3105 .position(|c| c.name == *name)
3106 .ok_or_else(|| {
3107 EngineError::Eval(EvalError::ColumnNotFound { name: name.clone() })
3108 })?;
3109 if map[idx].is_some() {
3110 return Err(EngineError::Storage(StorageError::ArityMismatch {
3111 expected: schema_cols_len,
3112 actual: cols.len(),
3113 }));
3114 }
3115 map[idx] = Some(j);
3116 }
3117 for (i, col) in column_meta.iter().enumerate() {
3121 if map[i].is_none()
3122 && !col.nullable
3123 && col.default.is_none()
3124 && !col.auto_increment
3125 {
3126 return Err(EngineError::Storage(StorageError::NullInNotNull {
3127 column: col.name.clone(),
3128 }));
3129 }
3130 }
3131 Some(map)
3132 }
3133 };
3134 let expected_tuple_len = stmt.columns.as_ref().map_or(schema_cols_len, Vec::len);
3135 let fks = table.schema().foreign_keys.clone();
3141 let mut affected = 0usize;
3142 let mut all_values: Vec<Vec<Value>> = Vec::with_capacity(stmt.rows.len());
3145 for tuple in stmt.rows {
3146 if tuple.len() != expected_tuple_len {
3147 return Err(EngineError::Storage(StorageError::ArityMismatch {
3148 expected: expected_tuple_len,
3149 actual: tuple.len(),
3150 }));
3151 }
3152 let values: Vec<Value> = if let Some(map) = &tuple_pos {
3156 let raw_tuple: Vec<Value> = tuple
3158 .into_iter()
3159 .map(literal_expr_to_value)
3160 .collect::<Result<_, _>>()?;
3161 let mut out = Vec::with_capacity(schema_cols_len);
3162 for (i, col) in column_meta.iter().enumerate() {
3163 let mut raw = match map[i] {
3164 Some(j) => raw_tuple[j].clone(),
3165 None => col.default.clone().unwrap_or(Value::Null),
3166 };
3167 if col.auto_increment && raw.is_null() {
3168 let next = table.next_auto_value(i).ok_or_else(|| {
3169 EngineError::Unsupported(alloc::format!(
3170 "AUTO_INCREMENT applies to integer columns only (column `{}`)",
3171 col.name
3172 ))
3173 })?;
3174 raw = Value::BigInt(next);
3175 }
3176 out.push(coerce_value(raw, col.ty, &col.name, i)?);
3177 }
3178 out
3179 } else {
3180 let mut out = Vec::with_capacity(schema_cols_len);
3182 for (i, (col, expr)) in column_meta.iter().zip(tuple).enumerate() {
3183 let mut raw = literal_expr_to_value(expr)?;
3184 if col.auto_increment && raw.is_null() {
3185 let next = table.next_auto_value(i).ok_or_else(|| {
3186 EngineError::Unsupported(alloc::format!(
3187 "AUTO_INCREMENT applies to integer columns only (column `{}`)",
3188 col.name
3189 ))
3190 })?;
3191 raw = Value::BigInt(next);
3192 }
3193 out.push(coerce_value(raw, col.ty, &col.name, i)?);
3194 }
3195 out
3196 };
3197 all_values.push(values);
3198 }
3199 let _ = table;
3204 if !fks.is_empty() {
3205 enforce_fk_inserts(self.active_catalog(), &stmt.table, &fks, &all_values)?;
3206 }
3207 let mut pending_updates: Vec<(usize, Vec<Value>)> = Vec::new();
3214 let mut skipped_count = 0usize;
3215 if let Some(clause) = &stmt.on_conflict {
3216 let conflict_cols = resolve_on_conflict_columns(
3217 self.active_catalog(),
3218 &stmt.table,
3219 clause.target_columns.as_slice(),
3220 )?;
3221 let mut kept: Vec<Vec<Value>> = Vec::with_capacity(all_values.len());
3222 let mut seen_keys: Vec<Vec<Value>> = Vec::new();
3223 for values in all_values {
3224 let key_tuple: Vec<&Value> =
3225 conflict_cols.iter().map(|&c| &values[c]).collect();
3226 let has_null_key = key_tuple.iter().any(|v| matches!(v, Value::Null));
3229 let collides_with_table = !has_null_key
3230 && on_conflict_keys_exist(
3231 self.active_catalog(),
3232 &stmt.table,
3233 &conflict_cols,
3234 &key_tuple,
3235 );
3236 let key_tuple_owned: Vec<Value> =
3237 key_tuple.iter().map(|v| (*v).clone()).collect();
3238 let collides_with_batch = !has_null_key
3239 && seen_keys.iter().any(|k| k == &key_tuple_owned);
3240 let collides = collides_with_table || collides_with_batch;
3241 match (&clause.action, collides) {
3242 (_, false) => {
3243 seen_keys.push(key_tuple_owned);
3244 kept.push(values);
3245 }
3246 (spg_sql::ast::OnConflictAction::Nothing, true) => {
3247 skipped_count += 1;
3248 }
3249 (
3250 spg_sql::ast::OnConflictAction::Update {
3251 assignments,
3252 where_,
3253 },
3254 true,
3255 ) => {
3256 if !collides_with_table {
3257 skipped_count += 1;
3258 continue;
3259 }
3260 let target_pos = lookup_row_position_by_keys(
3261 self.active_catalog(),
3262 &stmt.table,
3263 &conflict_cols,
3264 &key_tuple,
3265 )
3266 .ok_or_else(|| {
3267 EngineError::Unsupported(
3268 "ON CONFLICT DO UPDATE: conflict detected but row \
3269 position could not be resolved (cold-tier row?)"
3270 .into(),
3271 )
3272 })?;
3273 let updated = apply_on_conflict_assignments(
3274 self.active_catalog(),
3275 &stmt.table,
3276 target_pos,
3277 &values,
3278 assignments,
3279 where_.as_ref(),
3280 )?;
3281 if let Some(new_row) = updated {
3282 pending_updates.push((target_pos, new_row));
3283 } else {
3284 skipped_count += 1;
3285 }
3286 }
3287 }
3288 }
3289 all_values = kept;
3290 }
3291 let table = self
3293 .active_catalog_mut()
3294 .get_mut(&stmt.table)
3295 .ok_or_else(|| {
3296 EngineError::Storage(StorageError::TableNotFound {
3297 name: stmt.table.clone(),
3298 })
3299 })?;
3300 let mut returning_rows: Vec<Vec<Value>> = Vec::new();
3304 for values in all_values {
3305 if stmt.returning.is_some() {
3306 returning_rows.push(values.clone());
3307 }
3308 table.insert(Row::new(values))?;
3309 affected += 1;
3310 }
3311 for (pos, new_row) in pending_updates {
3315 if stmt.returning.is_some() {
3316 returning_rows.push(new_row.clone());
3317 }
3318 table.update_row(pos, new_row)?;
3319 affected += 1;
3320 }
3321 let _ = skipped_count;
3322 if let Some(items) = &stmt.returning {
3326 let _ = table;
3327 return self.build_returning_rows(
3328 &stmt.table,
3329 items,
3330 returning_rows,
3331 );
3332 }
3333 if !self.in_transaction() && affected > 0 {
3338 self.statistics
3339 .record_modifications(&stmt.table, affected as u64);
3340 }
3341 Ok(QueryResult::CommandOk {
3342 affected,
3343 modified_catalog: !self.in_transaction(),
3344 })
3345 }
3346
3347 fn exec_select_as_of_segment(
3360 &self,
3361 stmt: &SelectStatement,
3362 from: &spg_sql::ast::FromClause,
3363 segment_id: u32,
3364 ) -> Result<QueryResult, EngineError> {
3365 if !from.joins.is_empty()
3368 || stmt.group_by.is_some()
3369 || stmt.having.is_some()
3370 || !stmt.unions.is_empty()
3371 || !stmt.order_by.is_empty()
3372 || stmt.offset.is_some()
3373 || stmt.distinct
3374 || aggregate::uses_aggregate(stmt)
3375 {
3376 return Err(EngineError::Unsupported(
3377 "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
3378 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
3379 .into(),
3380 ));
3381 }
3382 let table = self
3383 .active_catalog()
3384 .get(&from.primary.name)
3385 .ok_or_else(|| StorageError::TableNotFound {
3386 name: from.primary.name.clone(),
3387 })?;
3388 let schema = table.schema().clone();
3389 let schema_cols = &schema.columns;
3390 let alias = from
3391 .primary
3392 .alias
3393 .as_deref()
3394 .unwrap_or(from.primary.name.as_str());
3395 let ctx = EvalContext::new(schema_cols, Some(alias));
3396 let seg = self
3397 .active_catalog()
3398 .cold_segment(segment_id)
3399 .ok_or_else(|| {
3400 EngineError::Unsupported(alloc::format!(
3401 "AS OF SEGMENT: cold segment {segment_id} not registered"
3402 ))
3403 })?;
3404 let mut out_rows: Vec<Row> = Vec::new();
3405 let mut limit_remaining: Option<usize> =
3406 stmt.limit.as_ref().and_then(|n| usize::try_from(*n).ok());
3407 for (_key, body) in seg.scan() {
3408 let (row, _consumed) = spg_storage::decode_row_body_dense(&body, &schema)
3409 .map_err(EngineError::Storage)?;
3410 if let Some(where_expr) = &stmt.where_ {
3411 let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
3412 if !matches!(cond, Value::Bool(true)) {
3413 continue;
3414 }
3415 }
3416 let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
3418 out_rows.push(projected);
3419 if let Some(rem) = limit_remaining.as_mut() {
3420 if *rem == 0 {
3421 out_rows.pop();
3422 break;
3423 }
3424 *rem -= 1;
3425 }
3426 }
3427 let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
3429 Ok(QueryResult::Rows {
3430 columns,
3431 rows: out_rows,
3432 })
3433 }
3434
3435 fn eval_expr_simple(
3440 &self,
3441 expr: &Expr,
3442 row: &Row,
3443 ctx: &EvalContext,
3444 ) -> Result<Value, EngineError> {
3445 let cancel = CancelToken::none();
3446 self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
3447 }
3448
3449 fn build_returning_rows(
3456 &self,
3457 table_name: &str,
3458 items: &[SelectItem],
3459 mutated_rows: Vec<Vec<Value>>,
3460 ) -> Result<QueryResult, EngineError> {
3461 let table = self.active_catalog().get(table_name).ok_or_else(|| {
3462 EngineError::Storage(StorageError::TableNotFound {
3463 name: table_name.into(),
3464 })
3465 })?;
3466 let schema_cols = table.schema().columns.clone();
3467 let columns = self.derive_output_columns(items, &schema_cols, table_name);
3468 let mut out_rows: Vec<Row> = Vec::with_capacity(mutated_rows.len());
3469 for values in mutated_rows {
3470 let row = Row::new(values);
3471 let projected = self.project_row_simple(&row, items, &schema_cols, table_name)?;
3472 out_rows.push(projected);
3473 }
3474 Ok(QueryResult::Rows {
3475 columns,
3476 rows: out_rows,
3477 })
3478 }
3479
3480 fn project_row_simple(
3484 &self,
3485 row: &Row,
3486 items: &[SelectItem],
3487 schema_cols: &[ColumnSchema],
3488 alias: &str,
3489 ) -> Result<Row, EngineError> {
3490 let ctx = EvalContext::new(schema_cols, Some(alias));
3491 let cancel = CancelToken::none();
3492 let mut out_vals = Vec::new();
3493 for item in items {
3494 match item {
3495 SelectItem::Wildcard => {
3496 out_vals.extend(row.values.iter().cloned());
3497 }
3498 SelectItem::Expr { expr, .. } => {
3499 let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
3500 out_vals.push(v);
3501 }
3502 }
3503 }
3504 Ok(Row::new(out_vals))
3505 }
3506
3507 fn derive_output_columns(
3512 &self,
3513 items: &[SelectItem],
3514 schema_cols: &[ColumnSchema],
3515 _alias: &str,
3516 ) -> Vec<ColumnSchema> {
3517 let mut out = Vec::new();
3518 for item in items {
3519 match item {
3520 SelectItem::Wildcard => {
3521 out.extend(schema_cols.iter().cloned());
3522 }
3523 SelectItem::Expr { alias, .. } => {
3524 let name = alias
3525 .clone()
3526 .unwrap_or_else(|| "?column?".to_string());
3527 out.push(ColumnSchema::new(name, DataType::Text, true));
3530 }
3531 }
3532 }
3533 out
3534 }
3535
3536 fn exec_select_cancel(
3537 &self,
3538 stmt: &SelectStatement,
3539 cancel: CancelToken<'_>,
3540 ) -> Result<QueryResult, EngineError> {
3541 cancel.check()?;
3542 if let Some(from) = &stmt.from
3551 && let Some(seg_id) = from.primary.as_of_segment
3552 {
3553 return self.exec_select_as_of_segment(stmt, from, seg_id);
3554 }
3555 if let Some(from) = &stmt.from
3559 && from.joins.is_empty()
3560 && stmt.where_.is_none()
3561 && stmt.group_by.is_none()
3562 && stmt.having.is_none()
3563 && stmt.unions.is_empty()
3564 && stmt.order_by.is_empty()
3565 && stmt.limit.is_none()
3566 && stmt.offset.is_none()
3567 && !stmt.distinct
3568 && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard))
3569 {
3570 let lower = from.primary.name.to_ascii_lowercase();
3571 match lower.as_str() {
3572 "spg_statistic" => return Ok(self.exec_spg_statistic()),
3573 "spg_stat_replication" => return Ok(self.exec_spg_stat_replication()),
3575 "spg_stat_segment" => return Ok(self.exec_spg_stat_segment()),
3576 "spg_stat_query" => return Ok(self.exec_spg_stat_query()),
3577 "spg_stat_activity" => return Ok(self.exec_spg_stat_activity()),
3578 "spg_audit_chain" => return Ok(self.exec_spg_audit_chain()),
3579 "spg_audit_verify" => return Ok(self.exec_spg_audit_verify()),
3580 "spg_table_ddl" => return Ok(self.exec_spg_table_ddl()),
3581 "spg_role_ddl" => return Ok(self.exec_spg_role_ddl()),
3582 "spg_database_ddl" => return Ok(self.exec_spg_database_ddl()),
3583 _ => {}
3584 }
3585 }
3586 if !stmt.ctes.is_empty() {
3594 return self.exec_with_ctes(stmt, cancel);
3595 }
3596 let mut stmt_owned;
3603 let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3604 stmt_owned = stmt.clone();
3605 self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3606 &stmt_owned
3607 } else {
3608 stmt
3609 };
3610 if stmt_ref.unions.is_empty() {
3611 return self.exec_bare_select_cancel(stmt_ref, cancel);
3612 }
3613 let mut head = stmt_ref.clone();
3618 head.unions = Vec::new();
3619 head.order_by = Vec::new();
3620 head.limit = None;
3621 let QueryResult::Rows { columns, mut rows } =
3622 self.exec_bare_select_cancel(&head, cancel)?
3623 else {
3624 unreachable!("bare SELECT cannot return CommandOk")
3625 };
3626 for (kind, peer) in &stmt_ref.unions {
3627 let QueryResult::Rows {
3628 columns: peer_cols,
3629 rows: peer_rows,
3630 } = self.exec_bare_select_cancel(peer, cancel)?
3631 else {
3632 unreachable!("bare SELECT cannot return CommandOk")
3633 };
3634 if peer_cols.len() != columns.len() {
3635 return Err(EngineError::Unsupported(alloc::format!(
3636 "UNION arity mismatch: head has {} columns, peer has {}",
3637 columns.len(),
3638 peer_cols.len()
3639 )));
3640 }
3641 rows.extend(peer_rows);
3642 if matches!(kind, UnionKind::Distinct) {
3643 rows = dedup_rows(rows);
3644 }
3645 }
3646 if !stmt.order_by.is_empty() {
3649 let synth_ctx = EvalContext::new(&columns, None);
3650 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
3651 let mut tagged: Vec<(Vec<f64>, Row)> = Vec::with_capacity(rows.len());
3652 for r in rows {
3653 let keys = build_order_keys(&stmt.order_by, &r, &synth_ctx)?;
3654 tagged.push((keys, r));
3655 }
3656 sort_by_keys(&mut tagged, &descs);
3657 rows = tagged.into_iter().map(|(_, r)| r).collect();
3658 }
3659 apply_offset_and_limit(&mut rows, stmt.offset, stmt.limit);
3660 Ok(QueryResult::Rows { columns, rows })
3661 }
3662
3663 #[allow(clippy::too_many_lines)]
3664 #[allow(clippy::too_many_lines)] fn exec_bare_select_cancel(
3666 &self,
3667 stmt: &SelectStatement,
3668 cancel: CancelToken<'_>,
3669 ) -> Result<QueryResult, EngineError> {
3670 if select_has_window(stmt) {
3675 return self.exec_select_with_window(stmt, cancel);
3676 }
3677 let Some(from) = &stmt.from else {
3682 let empty_schema: Vec<ColumnSchema> = Vec::new();
3683 let ctx = EvalContext::new(&empty_schema, None);
3684 let projection = build_projection(&stmt.items, &empty_schema, "")?;
3685 let dummy_row = Row::new(Vec::new());
3686 let mut values = Vec::with_capacity(projection.len());
3687 for p in &projection {
3688 values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
3689 }
3690 let columns: Vec<ColumnSchema> = projection
3691 .into_iter()
3692 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
3693 .collect();
3694 return Ok(QueryResult::Rows {
3695 columns,
3696 rows: alloc::vec![Row::new(values)],
3697 });
3698 };
3699 if !from.joins.is_empty() {
3703 return self.exec_joined_select(stmt, from);
3704 }
3705 let primary = &from.primary;
3706 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
3707 StorageError::TableNotFound {
3708 name: primary.name.clone(),
3709 }
3710 })?;
3711 let schema_cols = &table.schema().columns;
3712 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
3715 let ctx = EvalContext::new(schema_cols, Some(alias));
3716
3717 if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias) {
3722 return materialise_in_order(stmt, table, schema_cols, alias, &nsw_rows);
3723 }
3724
3725 let indexed_rows: Option<Vec<Cow<'_, Row>>> = stmt
3733 .where_
3734 .as_ref()
3735 .and_then(|w| try_index_seek(w, schema_cols, self.active_catalog(), table, alias));
3736
3737 if aggregate::uses_aggregate(stmt) {
3740 let mut filtered: Vec<&Row> = Vec::new();
3741 let mut memo = memoize::MemoizeCache::new();
3745 if let Some(rows) = &indexed_rows {
3746 for cow in rows {
3747 let row = cow.as_ref();
3748 if let Some(where_expr) = &stmt.where_ {
3749 let cond = self.eval_expr_with_correlated(
3750 where_expr,
3751 row,
3752 &ctx,
3753 cancel,
3754 Some(&mut memo),
3755 )?;
3756 if !matches!(cond, Value::Bool(true)) {
3757 continue;
3758 }
3759 }
3760 filtered.push(row);
3761 }
3762 } else {
3763 for i in 0..table.row_count() {
3764 let row = &table.rows()[i];
3765 if let Some(where_expr) = &stmt.where_ {
3766 let cond = self.eval_expr_with_correlated(
3767 where_expr,
3768 row,
3769 &ctx,
3770 cancel,
3771 Some(&mut memo),
3772 )?;
3773 if !matches!(cond, Value::Bool(true)) {
3774 continue;
3775 }
3776 }
3777 filtered.push(row);
3778 }
3779 }
3780 let mut agg = aggregate::run(stmt, &filtered, schema_cols, Some(alias))?;
3781 apply_offset_and_limit(&mut agg.rows, stmt.offset, stmt.limit);
3782 return Ok(QueryResult::Rows {
3783 columns: agg.columns,
3784 rows: agg.rows,
3785 });
3786 }
3787
3788 let projection = build_projection(&stmt.items, schema_cols, alias)?;
3789
3790 let mut tagged: Vec<(Vec<f64>, Row)> = Vec::new();
3793 let mut memo = memoize::MemoizeCache::new();
3795 let mut process_row = |row: &Row, loop_idx: usize| -> Result<(), EngineError> {
3798 if loop_idx.is_multiple_of(256) {
3799 cancel.check()?;
3800 }
3801 if let Some(where_expr) = &stmt.where_ {
3802 let cond = self.eval_expr_with_correlated(
3803 where_expr,
3804 row,
3805 &ctx,
3806 cancel,
3807 Some(&mut memo),
3808 )?;
3809 if !matches!(cond, Value::Bool(true)) {
3810 return Ok(());
3811 }
3812 }
3813 let mut values = Vec::with_capacity(projection.len());
3814 for p in &projection {
3815 values.push(eval::eval_expr(&p.expr, row, &ctx)?);
3816 }
3817 let order_keys = if stmt.order_by.is_empty() {
3818 Vec::new()
3819 } else {
3820 build_order_keys(&stmt.order_by, row, &ctx)?
3821 };
3822 tagged.push((order_keys, Row::new(values)));
3823 Ok(())
3824 };
3825 if let Some(rows) = &indexed_rows {
3826 for (loop_idx, cow) in rows.iter().enumerate() {
3827 process_row(cow.as_ref(), loop_idx)?;
3828 }
3829 } else {
3830 for i in 0..table.row_count() {
3831 process_row(&table.rows()[i], i)?;
3832 }
3833 }
3834
3835 if !stmt.order_by.is_empty() {
3836 let keep = if stmt.distinct {
3841 None
3842 } else {
3843 stmt.limit
3844 .map(|l| l as usize + stmt.offset.map_or(0, |o| o as usize))
3845 };
3846 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
3847 partial_sort_tagged(&mut tagged, keep, &descs);
3848 }
3849
3850 let mut output_rows: Vec<Row> = tagged.into_iter().map(|(_, r)| r).collect();
3851 if stmt.distinct {
3852 output_rows = dedup_rows(output_rows);
3853 }
3854 apply_offset_and_limit(&mut output_rows, stmt.offset, stmt.limit);
3855
3856 let columns: Vec<ColumnSchema> = projection
3857 .into_iter()
3858 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
3859 .collect();
3860
3861 Ok(QueryResult::Rows {
3862 columns,
3863 rows: output_rows,
3864 })
3865 }
3866
3867 #[allow(clippy::too_many_lines)]
3874 fn exec_joined_select(
3875 &self,
3876 stmt: &SelectStatement,
3877 from: &FromClause,
3878 ) -> Result<QueryResult, EngineError> {
3879 let primary_table = self
3882 .active_catalog()
3883 .get(&from.primary.name)
3884 .ok_or_else(|| StorageError::TableNotFound {
3885 name: from.primary.name.clone(),
3886 })?;
3887 let primary_alias = from
3888 .primary
3889 .alias
3890 .as_deref()
3891 .unwrap_or(from.primary.name.as_str())
3892 .to_string();
3893 let mut joined_tables: Vec<(&Table, String, JoinKind, Option<&Expr>)> = Vec::new();
3894 for j in &from.joins {
3895 let t = self.active_catalog().get(&j.table.name).ok_or_else(|| {
3896 StorageError::TableNotFound {
3897 name: j.table.name.clone(),
3898 }
3899 })?;
3900 let a = j
3901 .table
3902 .alias
3903 .as_deref()
3904 .unwrap_or(j.table.name.as_str())
3905 .to_string();
3906 joined_tables.push((t, a, j.kind, j.on.as_ref()));
3907 }
3908
3909 let mut combined_schema: Vec<ColumnSchema> = Vec::new();
3912 for col in &primary_table.schema().columns {
3913 combined_schema.push(ColumnSchema::new(
3914 alloc::format!("{primary_alias}.{}", col.name),
3915 col.ty,
3916 col.nullable,
3917 ));
3918 }
3919 for (t, a, _, _) in &joined_tables {
3920 for col in &t.schema().columns {
3921 combined_schema.push(ColumnSchema::new(
3922 alloc::format!("{a}.{}", col.name),
3923 col.ty,
3924 col.nullable,
3925 ));
3926 }
3927 }
3928 let ctx = EvalContext::new(&combined_schema, None);
3929
3930 let mut working: Vec<Row> = primary_table.rows().iter().cloned().collect();
3933 let mut produced_len = primary_table.schema().columns.len();
3934 for (t, _, kind, on) in &joined_tables {
3935 let right_arity = t.schema().columns.len();
3936 let mut next: Vec<Row> = Vec::new();
3937 for left in &working {
3938 let mut left_matched = false;
3939 for right in t.rows() {
3940 let mut combined_vals = left.values.clone();
3941 combined_vals.extend(right.values.iter().cloned());
3942 let combined = Row::new(combined_vals);
3945 let keep = if let Some(on_expr) = on {
3946 let cond = eval::eval_expr(on_expr, &combined, &ctx)?;
3947 matches!(cond, Value::Bool(true))
3948 } else {
3949 true
3951 };
3952 if keep {
3953 next.push(combined);
3954 left_matched = true;
3955 }
3956 }
3957 if !left_matched && matches!(kind, JoinKind::Left) {
3958 let mut combined_vals = left.values.clone();
3961 for _ in 0..right_arity {
3962 combined_vals.push(Value::Null);
3963 }
3964 next.push(Row::new(combined_vals));
3965 }
3966 }
3967 working = next;
3968 produced_len += right_arity;
3969 debug_assert!(produced_len <= combined_schema.len());
3970 }
3971
3972 let mut filtered: Vec<Row> = Vec::new();
3974 for row in working {
3975 if let Some(where_expr) = &stmt.where_ {
3976 let cond = eval::eval_expr(where_expr, &row, &ctx)?;
3977 if !matches!(cond, Value::Bool(true)) {
3978 continue;
3979 }
3980 }
3981 filtered.push(row);
3982 }
3983
3984 if aggregate::uses_aggregate(stmt) {
3987 let refs: Vec<&Row> = filtered.iter().collect();
3988 let mut agg = aggregate::run(stmt, &refs, &combined_schema, None)?;
3989 apply_offset_and_limit(&mut agg.rows, stmt.offset, stmt.limit);
3990 return Ok(QueryResult::Rows {
3991 columns: agg.columns,
3992 rows: agg.rows,
3993 });
3994 }
3995
3996 let projection = build_projection(&stmt.items, &combined_schema, "")?;
3997 let mut tagged: Vec<(Vec<f64>, Row)> = Vec::new();
3998 for row in &filtered {
3999 let mut values = Vec::with_capacity(projection.len());
4000 for p in &projection {
4001 values.push(eval::eval_expr(&p.expr, row, &ctx)?);
4002 }
4003 let order_keys = if stmt.order_by.is_empty() {
4004 Vec::new()
4005 } else {
4006 build_order_keys(&stmt.order_by, row, &ctx)?
4007 };
4008 tagged.push((order_keys, Row::new(values)));
4009 }
4010 if !stmt.order_by.is_empty() {
4011 let keep = if stmt.distinct {
4012 None
4013 } else {
4014 stmt.limit
4015 .map(|l| l as usize + stmt.offset.map_or(0, |o| o as usize))
4016 };
4017 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
4018 partial_sort_tagged(&mut tagged, keep, &descs);
4019 }
4020 let mut output_rows: Vec<Row> = tagged.into_iter().map(|(_, r)| r).collect();
4021 if stmt.distinct {
4022 output_rows = dedup_rows(output_rows);
4023 }
4024 apply_offset_and_limit(&mut output_rows, stmt.offset, stmt.limit);
4025 let columns: Vec<ColumnSchema> = projection
4026 .into_iter()
4027 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
4028 .collect();
4029 Ok(QueryResult::Rows {
4030 columns,
4031 rows: output_rows,
4032 })
4033 }
4034}
4035
4036#[derive(Debug, Clone)]
4039struct ProjectedItem {
4040 expr: Expr,
4041 output_name: String,
4042 ty: DataType,
4043 nullable: bool,
4044}
4045
4046fn dedup_rows(rows: Vec<Row>) -> Vec<Row> {
4052 let mut out: Vec<Row> = Vec::with_capacity(rows.len());
4053 for r in rows {
4054 if !out.iter().any(|seen| seen == &r) {
4055 out.push(r);
4056 }
4057 }
4058 out
4059}
4060
4061fn value_to_order_key(v: &Value) -> Result<f64, EngineError> {
4065 match v {
4066 Value::Null => Ok(f64::INFINITY),
4067 Value::SmallInt(n) => Ok(f64::from(*n)),
4068 Value::Int(n) => Ok(f64::from(*n)),
4069 Value::Date(d) => Ok(f64::from(*d)),
4070 #[allow(clippy::cast_precision_loss)]
4071 Value::Timestamp(t) => Ok(*t as f64),
4072 #[allow(clippy::cast_precision_loss)]
4073 Value::Numeric { scaled, scale } => {
4074 let mut divisor = 1.0_f64;
4080 for _ in 0..*scale {
4081 divisor *= 10.0;
4082 }
4083 Ok((*scaled as f64) / divisor)
4084 }
4085 #[allow(clippy::cast_precision_loss)]
4086 Value::BigInt(n) => Ok(*n as f64),
4087 Value::Float(x) => Ok(*x),
4088 Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
4089 Value::Text(s) => {
4090 let mut key: u64 = 0;
4094 for &b in s.as_bytes().iter().take(8) {
4095 key = (key << 8) | u64::from(b);
4096 }
4097 #[allow(clippy::cast_precision_loss)]
4098 Ok(key as f64)
4099 }
4100 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
4101 Err(EngineError::Unsupported(
4102 "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
4103 ))
4104 }
4105 Value::Interval { .. } => Err(EngineError::Unsupported(
4106 "ORDER BY of an INTERVAL is not supported in v2.11 \
4107 (months vs micros has no single canonical ordering)"
4108 .into(),
4109 )),
4110 Value::Json(_) => Err(EngineError::Unsupported(
4111 "ORDER BY of a JSON value is not supported — cast the document to text first".into(),
4112 )),
4113 _ => Err(EngineError::Unsupported(
4117 "ORDER BY of this value type is not supported".into(),
4118 )),
4119 }
4120}
4121
4122fn try_nsw_knn(
4136 stmt: &SelectStatement,
4137 table: &Table,
4138 schema_cols: &[ColumnSchema],
4139 table_alias: &str,
4140) -> Option<Vec<usize>> {
4141 if stmt.distinct {
4142 return None;
4143 }
4144 let limit = usize::try_from(stmt.limit?).ok()?;
4145 if limit == 0 {
4146 return None;
4147 }
4148 if stmt.order_by.len() != 1 {
4152 return None;
4153 }
4154 let order = &stmt.order_by[0];
4155 if order.desc {
4159 return None;
4160 }
4161 let Expr::Binary { lhs, op, rhs } = &order.expr else {
4162 return None;
4163 };
4164 let metric = match op {
4165 BinOp::L2Distance => spg_storage::NswMetric::L2,
4166 BinOp::InnerProduct => spg_storage::NswMetric::InnerProduct,
4167 BinOp::CosineDistance => spg_storage::NswMetric::Cosine,
4168 _ => return None,
4169 };
4170 let ((Expr::Column(col), literal) | (literal, Expr::Column(col))) =
4172 (lhs.as_ref(), rhs.as_ref())
4173 else {
4174 return None;
4175 };
4176 if let Some(q) = &col.qualifier
4177 && q != table_alias
4178 {
4179 return None;
4180 }
4181 let col_pos = schema_cols.iter().position(|s| s.name == col.name)?;
4182 let query = literal_to_vector(literal)?;
4183 let idx = spg_storage::nsw_index_on(table, col_pos)?;
4184 if let Some(where_expr) = &stmt.where_ {
4185 let over_fetch = limit.saturating_mul(10).max(NSW_OVER_FETCH_FLOOR);
4189 let candidates = spg_storage::nsw_query(table, &idx.name, &query, over_fetch, metric);
4190 let ctx = EvalContext::new(schema_cols, Some(table_alias));
4191 let mut kept: Vec<usize> = Vec::with_capacity(limit);
4192 for i in candidates {
4193 let row = &table.rows()[i];
4194 let cond = eval::eval_expr(where_expr, row, &ctx).ok()?;
4195 if matches!(cond, Value::Bool(true)) {
4196 kept.push(i);
4197 if kept.len() >= limit {
4198 break;
4199 }
4200 }
4201 }
4202 Some(kept)
4203 } else {
4204 Some(spg_storage::nsw_query(
4205 table, &idx.name, &query, limit, metric,
4206 ))
4207 }
4208}
4209
4210const NSW_OVER_FETCH_FLOOR: usize = 32;
4214
4215fn literal_to_vector(e: &Expr) -> Option<Vec<f32>> {
4218 match e {
4219 Expr::Literal(Literal::Vector(v)) => Some(v.clone()),
4220 Expr::Cast { expr, .. } => literal_to_vector(expr),
4221 _ => None,
4222 }
4223}
4224
4225fn materialise_in_order(
4229 stmt: &SelectStatement,
4230 table: &Table,
4231 schema_cols: &[ColumnSchema],
4232 table_alias: &str,
4233 ordered_rows: &[usize],
4234) -> Result<QueryResult, EngineError> {
4235 let ctx = EvalContext::new(schema_cols, Some(table_alias));
4236 let projection = build_projection(&stmt.items, schema_cols, table_alias)?;
4237 let mut output_rows: Vec<Row> = Vec::with_capacity(ordered_rows.len());
4238 for &i in ordered_rows {
4239 let row = &table.rows()[i];
4240 let mut values = Vec::with_capacity(projection.len());
4241 for p in &projection {
4242 values.push(eval::eval_expr(&p.expr, row, &ctx)?);
4243 }
4244 output_rows.push(Row::new(values));
4245 }
4246 apply_offset_and_limit(&mut output_rows, stmt.offset, stmt.limit);
4247 let columns: Vec<ColumnSchema> = projection
4248 .into_iter()
4249 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
4250 .collect();
4251 Ok(QueryResult::Rows {
4252 columns,
4253 rows: output_rows,
4254 })
4255}
4256
4257fn try_index_seek<'a>(
4258 where_expr: &Expr,
4259 schema_cols: &[ColumnSchema],
4260 catalog: &'a Catalog,
4261 table: &'a Table,
4262 table_alias: &str,
4263) -> Option<Vec<Cow<'a, Row>>> {
4264 let Expr::Binary {
4265 lhs,
4266 op: BinOp::Eq,
4267 rhs,
4268 } = where_expr
4269 else {
4270 return None;
4271 };
4272 let (col_pos, value) = resolve_col_literal_pair(lhs, rhs, schema_cols, table_alias)
4273 .or_else(|| resolve_col_literal_pair(rhs, lhs, schema_cols, table_alias))?;
4274 let idx = table.index_on(col_pos)?;
4275 let key = IndexKey::from_value(&value)?;
4276 let locators = idx.lookup_eq(&key);
4277 let table_name = table.schema().name.as_str();
4278 let mut out: Vec<Cow<'a, Row>> = Vec::with_capacity(locators.len());
4286 for loc in locators {
4287 match *loc {
4288 spg_storage::RowLocator::Hot(i) => {
4289 if let Some(row) = table.rows().get(i) {
4290 out.push(Cow::Borrowed(row));
4291 }
4292 }
4293 spg_storage::RowLocator::Cold { segment_id, .. } => {
4294 if let Some(row) = catalog.resolve_cold_locator(table_name, segment_id, &key) {
4295 out.push(Cow::Owned(row));
4296 }
4297 }
4298 }
4299 }
4300 Some(out)
4301}
4302
4303fn try_pk_predicate(
4315 where_expr: &Expr,
4316 schema_cols: &[ColumnSchema],
4317 table_alias: &str,
4318) -> Option<(usize, IndexKey)> {
4319 let Expr::Binary {
4320 lhs,
4321 op: BinOp::Eq,
4322 rhs,
4323 } = where_expr
4324 else {
4325 return None;
4326 };
4327 let (col_pos, value) = resolve_col_literal_pair(lhs, rhs, schema_cols, table_alias)
4328 .or_else(|| resolve_col_literal_pair(rhs, lhs, schema_cols, table_alias))?;
4329 let key = IndexKey::from_value(&value)?;
4330 Some((col_pos, key))
4331}
4332
4333fn resolve_col_literal_pair(
4334 col_side: &Expr,
4335 lit_side: &Expr,
4336 schema_cols: &[ColumnSchema],
4337 table_alias: &str,
4338) -> Option<(usize, Value)> {
4339 let Expr::Column(c) = col_side else {
4340 return None;
4341 };
4342 if let Some(q) = &c.qualifier
4343 && q != table_alias
4344 {
4345 return None;
4346 }
4347 let pos = schema_cols.iter().position(|s| s.name == c.name)?;
4348 let Expr::Literal(l) = lit_side else {
4349 return None;
4350 };
4351 let v = match l {
4352 Literal::Integer(n) => {
4353 if let Ok(small) = i32::try_from(*n) {
4354 Value::Int(small)
4355 } else {
4356 Value::BigInt(*n)
4357 }
4358 }
4359 Literal::Float(x) => Value::Float(*x),
4360 Literal::String(s) => Value::Text(s.clone()),
4361 Literal::Bool(b) => Value::Bool(*b),
4362 Literal::Null => Value::Null,
4363 Literal::Vector(_) | Literal::Interval { .. } => return None,
4366 };
4367 Some((pos, v))
4368}
4369
4370fn resolve_projection_column<'a>(
4375 c: &ColumnName,
4376 schema_cols: &'a [ColumnSchema],
4377 table_alias: &str,
4378) -> Result<&'a ColumnSchema, EngineError> {
4379 if let Some(q) = &c.qualifier {
4380 let composite = alloc::format!("{q}.{name}", name = c.name);
4381 if let Some(s) = schema_cols.iter().find(|s| s.name == composite) {
4382 return Ok(s);
4383 }
4384 if q == table_alias
4387 && let Some(s) = schema_cols.iter().find(|s| s.name == c.name)
4388 {
4389 return Ok(s);
4390 }
4391 let prefix = alloc::format!("{q}.");
4395 let qualifier_known =
4396 q == table_alias || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
4397 if !qualifier_known {
4398 return Err(EngineError::Eval(EvalError::UnknownQualifier {
4399 qualifier: q.clone(),
4400 }));
4401 }
4402 return Err(EngineError::Eval(EvalError::ColumnNotFound {
4403 name: c.name.clone(),
4404 }));
4405 }
4406 if let Some(s) = schema_cols.iter().find(|s| s.name == c.name) {
4407 return Ok(s);
4408 }
4409 let suffix = alloc::format!(".{name}", name = c.name);
4410 let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
4411 let first = matches.next();
4412 let extra = matches.next();
4413 match (first, extra) {
4414 (Some(s), None) => Ok(s),
4415 (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
4416 detail: alloc::format!("ambiguous column reference: {}", c.name),
4417 })),
4418 _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
4419 name: c.name.clone(),
4420 })),
4421 }
4422}
4423
4424fn build_projection(
4425 items: &[SelectItem],
4426 schema_cols: &[ColumnSchema],
4427 table_alias: &str,
4428) -> Result<Vec<ProjectedItem>, EngineError> {
4429 let mut out = Vec::new();
4430 for item in items {
4431 match item {
4432 SelectItem::Wildcard => {
4433 for col in schema_cols {
4434 out.push(ProjectedItem {
4435 expr: Expr::Column(ColumnName {
4436 qualifier: None,
4437 name: col.name.clone(),
4438 }),
4439 output_name: col.name.clone(),
4440 ty: col.ty,
4441 nullable: col.nullable,
4442 });
4443 }
4444 }
4445 SelectItem::Expr { expr, alias } => {
4446 if let Expr::Column(c) = expr {
4451 let sch = resolve_projection_column(c, schema_cols, table_alias)?;
4452 let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
4453 out.push(ProjectedItem {
4454 expr: expr.clone(),
4455 output_name,
4456 ty: sch.ty,
4457 nullable: sch.nullable,
4458 });
4459 } else {
4460 let output_name = alias.clone().unwrap_or_else(|| expr.to_string());
4461 out.push(ProjectedItem {
4462 expr: expr.clone(),
4463 output_name,
4464 ty: DataType::Text,
4465 nullable: true,
4466 });
4467 }
4468 }
4469 }
4470 }
4471 Ok(out)
4472}
4473
4474fn numeric_from_integer(
4478 n: i128,
4479 precision: u8,
4480 scale: u8,
4481 col_name: &str,
4482) -> Result<Value, EngineError> {
4483 let factor = pow10_i128(scale);
4484 let scaled = n.checked_mul(factor).ok_or_else(|| {
4485 EngineError::Unsupported(alloc::format!(
4486 "integer overflow scaling value for column `{col_name}` to scale {scale}"
4487 ))
4488 })?;
4489 check_precision(scaled, precision, col_name)?;
4490 Ok(Value::Numeric { scaled, scale })
4491}
4492
4493#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
4496fn numeric_from_float(
4497 x: f64,
4498 precision: u8,
4499 scale: u8,
4500 col_name: &str,
4501) -> Result<Value, EngineError> {
4502 if !x.is_finite() {
4503 return Err(EngineError::Unsupported(alloc::format!(
4504 "cannot store non-finite float in NUMERIC column `{col_name}`"
4505 )));
4506 }
4507 let mut factor = 1.0_f64;
4508 for _ in 0..scale {
4509 factor *= 10.0;
4510 }
4511 let shifted = x * factor;
4516 let biased = if shifted >= 0.0 {
4517 shifted + 0.5
4518 } else {
4519 shifted - 0.5
4520 };
4521 if !(-1e38..=1e38).contains(&biased) {
4524 return Err(EngineError::Unsupported(alloc::format!(
4525 "value {x} overflows NUMERIC range for column `{col_name}`"
4526 )));
4527 }
4528 let scaled = biased as i128;
4529 check_precision(scaled, precision, col_name)?;
4530 Ok(Value::Numeric { scaled, scale })
4531}
4532
4533fn numeric_rescale(
4536 scaled: i128,
4537 src_scale: u8,
4538 precision: u8,
4539 dst_scale: u8,
4540 col_name: &str,
4541) -> Result<Value, EngineError> {
4542 let new_scaled = if dst_scale >= src_scale {
4543 let bump = pow10_i128(dst_scale - src_scale);
4544 scaled.checked_mul(bump).ok_or_else(|| {
4545 EngineError::Unsupported(alloc::format!(
4546 "overflow rescaling NUMERIC for column `{col_name}`"
4547 ))
4548 })?
4549 } else {
4550 let drop = pow10_i128(src_scale - dst_scale);
4551 let half = drop / 2;
4552 if scaled >= 0 {
4553 (scaled + half) / drop
4554 } else {
4555 (scaled - half) / drop
4556 }
4557 };
4558 check_precision(new_scaled, precision, col_name)?;
4559 Ok(Value::Numeric {
4560 scaled: new_scaled,
4561 scale: dst_scale,
4562 })
4563}
4564
4565const fn numeric_truncate_to_integer(scaled: i128, scale: u8) -> i128 {
4568 if scale == 0 {
4569 return scaled;
4570 }
4571 let factor = pow10_i128_const(scale);
4572 scaled / factor
4573}
4574
4575fn check_precision(scaled: i128, precision: u8, col_name: &str) -> Result<(), EngineError> {
4579 if precision == 0 {
4580 return Ok(());
4581 }
4582 let limit = pow10_i128(precision);
4583 if scaled.unsigned_abs() >= limit.unsigned_abs() {
4584 return Err(EngineError::Unsupported(alloc::format!(
4585 "NUMERIC value exceeds precision {precision} for column `{col_name}`"
4586 )));
4587 }
4588 Ok(())
4589}
4590
4591const fn pow10_i128_const(p: u8) -> i128 {
4592 let mut acc: i128 = 1;
4593 let mut i = 0;
4594 while i < p {
4595 acc *= 10;
4596 i += 1;
4597 }
4598 acc
4599}
4600
4601fn pow10_i128(p: u8) -> i128 {
4602 pow10_i128_const(p)
4603}
4604
4605impl Engine {
4620 #[allow(
4631 clippy::too_many_lines,
4632 clippy::type_complexity,
4633 clippy::needless_range_loop
4634 )] fn exec_select_with_window(
4636 &self,
4637 stmt: &SelectStatement,
4638 cancel: CancelToken<'_>,
4639 ) -> Result<QueryResult, EngineError> {
4640 let from = stmt.from.as_ref().ok_or_else(|| {
4641 EngineError::Unsupported("window functions require a FROM clause".into())
4642 })?;
4643 if !from.joins.is_empty() {
4646 return Err(EngineError::Unsupported(
4647 "JOIN with window functions not yet supported".into(),
4648 ));
4649 }
4650 let primary = &from.primary;
4651 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4652 StorageError::TableNotFound {
4653 name: primary.name.clone(),
4654 }
4655 })?;
4656 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4657 let schema_cols = &table.schema().columns;
4658 let ctx = EvalContext::new(schema_cols, Some(alias));
4659
4660 let mut filtered: Vec<&Row> = Vec::new();
4662 for (i, row) in table.rows().iter().enumerate() {
4663 if i.is_multiple_of(256) {
4664 cancel.check()?;
4665 }
4666 if let Some(w) = &stmt.where_ {
4667 let cond = eval::eval_expr(w, row, &ctx)?;
4668 if !matches!(cond, Value::Bool(true)) {
4669 continue;
4670 }
4671 }
4672 filtered.push(row);
4673 }
4674 let n_rows = filtered.len();
4675
4676 let mut window_nodes: Vec<Expr> = Vec::new();
4678 for item in &stmt.items {
4679 if let SelectItem::Expr { expr, .. } = item {
4680 collect_window_nodes(expr, &mut window_nodes);
4681 }
4682 }
4683
4684 let mut win_vals: Vec<Vec<Value>> = Vec::with_capacity(window_nodes.len());
4687 for wnode in &window_nodes {
4688 let Expr::WindowFunction {
4689 name,
4690 args,
4691 partition_by,
4692 order_by,
4693 frame,
4694 null_treatment,
4695 } = wnode
4696 else {
4697 unreachable!("collect_window_nodes pushes only WindowFunction");
4698 };
4699 let mut indexed: Vec<(Vec<Value>, Vec<(Value, bool)>, usize)> =
4701 Vec::with_capacity(n_rows);
4702 for (i, row) in filtered.iter().enumerate() {
4703 let pkey: Vec<Value> = partition_by
4704 .iter()
4705 .map(|p| eval::eval_expr(p, row, &ctx))
4706 .collect::<Result<_, _>>()?;
4707 let okey: Vec<(Value, bool)> = order_by
4708 .iter()
4709 .map(|(e, desc)| eval::eval_expr(e, row, &ctx).map(|v| (v, *desc)))
4710 .collect::<Result<_, _>>()?;
4711 indexed.push((pkey, okey, i));
4712 }
4713 indexed.sort_by(|a, b| {
4716 let p_cmp = partition_key_cmp(&a.0, &b.0);
4717 if p_cmp != core::cmp::Ordering::Equal {
4718 return p_cmp;
4719 }
4720 order_key_cmp(&a.1, &b.1)
4721 });
4722 let mut out_vals: Vec<Value> = alloc::vec![Value::Null; n_rows];
4724 let mut p_start = 0;
4725 while p_start < indexed.len() {
4726 let mut p_end = p_start + 1;
4727 while p_end < indexed.len()
4728 && partition_key_cmp(&indexed[p_start].0, &indexed[p_end].0)
4729 == core::cmp::Ordering::Equal
4730 {
4731 p_end += 1;
4732 }
4733 compute_window_partition(
4735 name,
4736 args,
4737 !order_by.is_empty(),
4738 frame.as_ref(),
4739 *null_treatment,
4740 &indexed[p_start..p_end],
4741 &filtered,
4742 &ctx,
4743 &mut out_vals,
4744 )?;
4745 p_start = p_end;
4746 }
4747 win_vals.push(out_vals);
4748 }
4749
4750 let mut ext_cols = schema_cols.clone();
4752 for i in 0..window_nodes.len() {
4753 ext_cols.push(ColumnSchema::new(
4754 alloc::format!("__win_{i}"),
4755 DataType::Text, true,
4757 ));
4758 }
4759 let mut ext_rows: Vec<Row> = Vec::with_capacity(n_rows);
4761 for i in 0..n_rows {
4762 let mut values = filtered[i].values.clone();
4763 for w in 0..window_nodes.len() {
4764 values.push(win_vals[w][i].clone());
4765 }
4766 ext_rows.push(Row::new(values));
4767 }
4768 let mut rewritten_items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
4770 for item in &stmt.items {
4771 let new_item = match item {
4772 SelectItem::Wildcard => SelectItem::Wildcard,
4773 SelectItem::Expr { expr, alias } => {
4774 let mut e = expr.clone();
4775 rewrite_window_to_columns(&mut e, &window_nodes);
4776 SelectItem::Expr {
4777 expr: e,
4778 alias: alias.clone(),
4779 }
4780 }
4781 };
4782 rewritten_items.push(new_item);
4783 }
4784
4785 let ext_ctx = EvalContext::new(&ext_cols, Some(alias));
4787 let projection = build_projection(&rewritten_items, &ext_cols, alias)?;
4788 let mut tagged: Vec<(Vec<f64>, Row)> = Vec::with_capacity(n_rows);
4789 for (i, row) in ext_rows.iter().enumerate() {
4790 if i.is_multiple_of(256) {
4791 cancel.check()?;
4792 }
4793 let mut values = Vec::with_capacity(projection.len());
4794 for p in &projection {
4795 values.push(eval::eval_expr(&p.expr, row, &ext_ctx)?);
4796 }
4797 let order_keys = if stmt.order_by.is_empty() {
4798 Vec::new()
4799 } else {
4800 let mut keys = Vec::with_capacity(stmt.order_by.len());
4801 for o in &stmt.order_by {
4802 let mut e = o.expr.clone();
4803 rewrite_window_to_columns(&mut e, &window_nodes);
4804 let key = eval::eval_expr(&e, row, &ext_ctx)?;
4805 keys.push(value_to_order_key(&key)?);
4806 }
4807 keys
4808 };
4809 tagged.push((order_keys, Row::new(values)));
4810 }
4811 if !stmt.order_by.is_empty() {
4813 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
4814 sort_by_keys(&mut tagged, &descs);
4815 }
4816 let mut out_rows: Vec<Row> = tagged.into_iter().map(|(_, r)| r).collect();
4817 apply_offset_and_limit(&mut out_rows, stmt.offset, stmt.limit);
4818 let final_cols: Vec<ColumnSchema> = projection
4819 .into_iter()
4820 .map(|p| ColumnSchema::new(p.output_name, p.ty, p.nullable))
4821 .collect();
4822 Ok(QueryResult::Rows {
4823 columns: final_cols,
4824 rows: out_rows,
4825 })
4826 }
4827
4828 fn exec_with_ctes(
4835 &self,
4836 stmt: &SelectStatement,
4837 cancel: CancelToken<'_>,
4838 ) -> Result<QueryResult, EngineError> {
4839 cancel.check()?;
4840 let mut catalog = self.active_catalog().clone();
4841 for cte in &stmt.ctes {
4842 if catalog.get(&cte.name).is_some() {
4843 return Err(EngineError::Unsupported(alloc::format!(
4844 "CTE name {:?} shadows an existing table; rename the CTE",
4845 cte.name
4846 )));
4847 }
4848 let (columns, rows) = if cte.recursive {
4849 self.materialise_recursive_cte(cte, &catalog, cancel)?
4850 } else {
4851 let body_result = self.exec_select_cancel(&cte.body, cancel)?;
4852 let QueryResult::Rows { columns, rows } = body_result else {
4853 return Err(EngineError::Unsupported(alloc::format!(
4854 "CTE {:?} body did not return rows",
4855 cte.name
4856 )));
4857 };
4858 (columns, rows)
4859 };
4860 let inferred = infer_column_types(&columns, &rows);
4865 let mut columns = inferred;
4866 if !cte.column_overrides.is_empty() {
4868 if cte.column_overrides.len() != columns.len() {
4869 return Err(EngineError::Unsupported(alloc::format!(
4870 "CTE {:?} column list has {} names but body returns {} columns",
4871 cte.name,
4872 cte.column_overrides.len(),
4873 columns.len()
4874 )));
4875 }
4876 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
4877 col.name.clone_from(name);
4878 }
4879 }
4880 let schema = TableSchema::new(cte.name.clone(), columns);
4881 catalog.create_table(schema).map_err(EngineError::Storage)?;
4882 let table = catalog
4883 .get_mut(&cte.name)
4884 .expect("just-created CTE table must exist");
4885 for row in rows {
4886 table.insert(row).map_err(EngineError::Storage)?;
4887 }
4888 }
4889 let mut body = stmt.clone();
4892 body.ctes = Vec::new();
4893 let mut temp = Engine::restore(catalog);
4894 if let Some(c) = self.clock {
4895 temp = temp.with_clock(c);
4896 }
4897 if let Some(f) = self.salt_fn {
4898 temp = temp.with_salt_fn(f);
4899 }
4900 temp.exec_select_cancel(&body, cancel)
4901 }
4902
4903 #[allow(clippy::too_many_lines)]
4913 fn materialise_recursive_cte(
4914 &self,
4915 cte: &spg_sql::ast::Cte,
4916 base_catalog: &Catalog,
4917 cancel: CancelToken<'_>,
4918 ) -> Result<(Vec<ColumnSchema>, Vec<Row>), EngineError> {
4919 const MAX_TOTAL_ROWS: usize = 1_000_000;
4920 const MAX_ITERATIONS: usize = 100_000;
4921 cancel.check()?;
4922 if cte.body.unions.is_empty() {
4923 return Err(EngineError::Unsupported(alloc::format!(
4924 "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
4925 cte.name
4926 )));
4927 }
4928 let mut anchor = cte.body.clone();
4930 let union_terms = core::mem::take(&mut anchor.unions);
4931 anchor.ctes = Vec::new();
4932 if select_refers_to(&anchor, &cte.name) {
4934 return Err(EngineError::Unsupported(alloc::format!(
4935 "WITH RECURSIVE {:?}: the anchor must not reference the CTE itself",
4936 cte.name
4937 )));
4938 }
4939 let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
4940 let QueryResult::Rows {
4941 columns: anchor_cols,
4942 rows: anchor_rows,
4943 } = anchor_result
4944 else {
4945 return Err(EngineError::Unsupported(alloc::format!(
4946 "WITH RECURSIVE {:?}: anchor did not return rows",
4947 cte.name
4948 )));
4949 };
4950 let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
4954 if !cte.column_overrides.is_empty() {
4955 if cte.column_overrides.len() != columns.len() {
4956 return Err(EngineError::Unsupported(alloc::format!(
4957 "CTE {:?} column list has {} names but anchor returns {} columns",
4958 cte.name,
4959 cte.column_overrides.len(),
4960 columns.len()
4961 )));
4962 }
4963 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
4964 col.name.clone_from(name);
4965 }
4966 }
4967 let mut all_rows: Vec<Row> = anchor_rows.clone();
4968 let mut working_set: Vec<Row> = anchor_rows;
4969 let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
4970 let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
4973 if !all_union_all {
4974 for r in &all_rows {
4975 seen.insert(encode_row_key(r));
4976 }
4977 }
4978 for iter in 0..MAX_ITERATIONS {
4979 cancel.check()?;
4980 if working_set.is_empty() {
4981 break;
4982 }
4983 let mut iter_catalog = base_catalog.clone();
4985 let schema = TableSchema::new(cte.name.clone(), columns.clone());
4986 iter_catalog
4987 .create_table(schema)
4988 .map_err(EngineError::Storage)?;
4989 {
4990 let table = iter_catalog.get_mut(&cte.name).expect("just-created");
4991 for row in &working_set {
4992 table.insert(row.clone()).map_err(EngineError::Storage)?;
4993 }
4994 }
4995 let mut iter_engine = Engine::restore(iter_catalog);
4996 if let Some(c) = self.clock {
4997 iter_engine = iter_engine.with_clock(c);
4998 }
4999 if let Some(f) = self.salt_fn {
5000 iter_engine = iter_engine.with_salt_fn(f);
5001 }
5002 let mut next_set: Vec<Row> = Vec::new();
5004 for (_, term) in &union_terms {
5005 let mut term = term.clone();
5006 term.ctes = Vec::new();
5007 let r = iter_engine.exec_select_cancel(&term, cancel)?;
5008 let QueryResult::Rows {
5009 columns: rc,
5010 rows: rs,
5011 } = r
5012 else {
5013 return Err(EngineError::Unsupported(alloc::format!(
5014 "WITH RECURSIVE {:?}: recursive term did not return rows",
5015 cte.name
5016 )));
5017 };
5018 if rc.len() != columns.len() {
5019 return Err(EngineError::Unsupported(alloc::format!(
5020 "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
5021 cte.name,
5022 rc.len(),
5023 columns.len()
5024 )));
5025 }
5026 for row in rs {
5027 if !all_union_all {
5028 let key = encode_row_key(&row);
5029 if !seen.insert(key) {
5030 continue;
5031 }
5032 }
5033 next_set.push(row);
5034 }
5035 }
5036 if next_set.is_empty() {
5037 break;
5038 }
5039 all_rows.extend(next_set.iter().cloned());
5040 working_set = next_set;
5041 if all_rows.len() > MAX_TOTAL_ROWS {
5042 return Err(EngineError::Unsupported(alloc::format!(
5043 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
5044 cte.name
5045 )));
5046 }
5047 if iter + 1 == MAX_ITERATIONS {
5048 return Err(EngineError::Unsupported(alloc::format!(
5049 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
5050 cte.name
5051 )));
5052 }
5053 }
5054 Ok((columns, all_rows))
5055 }
5056
5057 fn resolve_select_subqueries(
5058 &self,
5059 stmt: &mut SelectStatement,
5060 cancel: CancelToken<'_>,
5061 ) -> Result<(), EngineError> {
5062 for item in &mut stmt.items {
5063 if let SelectItem::Expr { expr, .. } = item {
5064 self.resolve_expr_subqueries(expr, cancel)?;
5065 }
5066 }
5067 if let Some(w) = &mut stmt.where_ {
5068 self.resolve_expr_subqueries(w, cancel)?;
5069 }
5070 if let Some(gs) = &mut stmt.group_by {
5071 for g in gs {
5072 self.resolve_expr_subqueries(g, cancel)?;
5073 }
5074 }
5075 if let Some(h) = &mut stmt.having {
5076 self.resolve_expr_subqueries(h, cancel)?;
5077 }
5078 for o in &mut stmt.order_by {
5079 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
5080 }
5081 for (_, peer) in &mut stmt.unions {
5082 self.resolve_select_subqueries(peer, cancel)?;
5083 }
5084 Ok(())
5085 }
5086
5087 #[allow(clippy::only_used_in_recursion)] fn resolve_expr_subqueries(
5089 &self,
5090 e: &mut Expr,
5091 cancel: CancelToken<'_>,
5092 ) -> Result<(), EngineError> {
5093 if let Some(replacement) = self.subquery_replacement(e, cancel)? {
5095 *e = replacement;
5096 return Ok(());
5097 }
5098 match e {
5099 Expr::Binary { lhs, rhs, .. } => {
5100 self.resolve_expr_subqueries(lhs, cancel)?;
5101 self.resolve_expr_subqueries(rhs, cancel)?;
5102 }
5103 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
5104 self.resolve_expr_subqueries(expr, cancel)?;
5105 }
5106 Expr::FunctionCall { args, .. } => {
5107 for a in args {
5108 self.resolve_expr_subqueries(a, cancel)?;
5109 }
5110 }
5111 Expr::Like { expr, pattern, .. } => {
5112 self.resolve_expr_subqueries(expr, cancel)?;
5113 self.resolve_expr_subqueries(pattern, cancel)?;
5114 }
5115 Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
5116 Expr::WindowFunction {
5119 args,
5120 partition_by,
5121 order_by,
5122 ..
5123 } => {
5124 for a in args {
5125 self.resolve_expr_subqueries(a, cancel)?;
5126 }
5127 for p in partition_by {
5128 self.resolve_expr_subqueries(p, cancel)?;
5129 }
5130 for (e, _) in order_by {
5131 self.resolve_expr_subqueries(e, cancel)?;
5132 }
5133 }
5134 Expr::ScalarSubquery(_)
5138 | Expr::Exists { .. }
5139 | Expr::InSubquery { .. }
5140 | Expr::Literal(_)
5141 | Expr::Placeholder(_)
5142 | Expr::Column(_) => {}
5143 }
5144 Ok(())
5145 }
5146
5147 fn eval_expr_with_correlated(
5155 &self,
5156 expr: &Expr,
5157 row: &Row,
5158 ctx: &EvalContext<'_>,
5159 cancel: CancelToken<'_>,
5160 memo: Option<&mut memoize::MemoizeCache>,
5161 ) -> Result<Value, EngineError> {
5162 if !expr_has_subquery(expr) {
5163 return eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval);
5164 }
5165 let mut e = expr.clone();
5166 self.resolve_correlated_in_expr(&mut e, row, ctx, cancel, memo)?;
5167 eval::eval_expr(&e, row, ctx).map_err(EngineError::Eval)
5168 }
5169
5170 fn resolve_correlated_in_expr(
5171 &self,
5172 e: &mut Expr,
5173 row: &Row,
5174 ctx: &EvalContext<'_>,
5175 cancel: CancelToken<'_>,
5176 mut memo: Option<&mut memoize::MemoizeCache>,
5177 ) -> Result<(), EngineError> {
5178 match e {
5179 Expr::ScalarSubquery(inner) => {
5180 let cache_key = memo.as_ref().map(|_| memoize::CacheKey {
5185 subquery_repr: alloc::format!("{}", **inner),
5186 outer_values: row.values.clone(),
5187 });
5188 if let (Some(cache), Some(k)) = (memo.as_deref_mut(), cache_key.as_ref())
5189 && let Some(cached) = cache.get(k)
5190 {
5191 *e = value_to_literal_expr(cached)?;
5192 return Ok(());
5193 }
5194 let mut s = (**inner).clone();
5195 substitute_outer_columns(&mut s, row, ctx);
5196 let r = self.exec_select_cancel(&s, cancel)?;
5197 let QueryResult::Rows { rows, .. } = r else {
5198 return Err(EngineError::Unsupported(
5199 "scalar subquery: inner did not return rows".into(),
5200 ));
5201 };
5202 let value = match rows.as_slice() {
5203 [] => Value::Null,
5204 [r0] => r0.values.first().cloned().unwrap_or(Value::Null),
5205 _ => {
5206 return Err(EngineError::Unsupported(alloc::format!(
5207 "scalar subquery returned {} rows; expected 0 or 1",
5208 rows.len()
5209 )));
5210 }
5211 };
5212 if let (Some(cache), Some(k)) = (memo.as_deref_mut(), cache_key) {
5213 cache.insert(k, value.clone());
5214 }
5215 *e = value_to_literal_expr(value)?;
5216 }
5217 Expr::Exists { subquery, negated } => {
5218 let mut s = (**subquery).clone();
5219 substitute_outer_columns(&mut s, row, ctx);
5220 let r = self.exec_select_cancel(&s, cancel)?;
5221 let exists = matches!(r, QueryResult::Rows { rows, .. } if !rows.is_empty());
5222 let bit = if *negated { !exists } else { exists };
5223 *e = Expr::Literal(Literal::Bool(bit));
5224 }
5225 Expr::InSubquery {
5226 expr: lhs,
5227 subquery,
5228 negated,
5229 } => {
5230 self.resolve_correlated_in_expr(lhs, row, ctx, cancel, memo.as_deref_mut())?;
5231 let lhs_val = eval::eval_expr(lhs, row, ctx).map_err(EngineError::Eval)?;
5232 let mut s = (**subquery).clone();
5233 substitute_outer_columns(&mut s, row, ctx);
5234 let r = self.exec_select_cancel(&s, cancel)?;
5235 let QueryResult::Rows { columns, rows, .. } = r else {
5236 return Err(EngineError::Unsupported(
5237 "IN-subquery: inner did not return rows".into(),
5238 ));
5239 };
5240 if columns.len() != 1 {
5241 return Err(EngineError::Unsupported(alloc::format!(
5242 "IN-subquery must project exactly one column; got {}",
5243 columns.len()
5244 )));
5245 }
5246 let mut found = false;
5247 let mut any_null = false;
5248 for r0 in rows {
5249 let v = r0.values.into_iter().next().unwrap_or(Value::Null);
5250 if v.is_null() {
5251 any_null = true;
5252 continue;
5253 }
5254 if value_cmp(&v, &lhs_val) == core::cmp::Ordering::Equal {
5255 found = true;
5256 break;
5257 }
5258 }
5259 let bit = if found {
5260 !*negated
5261 } else if any_null {
5262 return Err(EngineError::Unsupported(
5263 "IN-subquery with NULL in result and no match: NULL semantics not yet implemented".into(),
5264 ));
5265 } else {
5266 *negated
5267 };
5268 *e = Expr::Literal(Literal::Bool(bit));
5269 }
5270 Expr::Binary { lhs, rhs, .. } => {
5271 self.resolve_correlated_in_expr(lhs, row, ctx, cancel, memo.as_deref_mut())?;
5272 self.resolve_correlated_in_expr(rhs, row, ctx, cancel, memo.as_deref_mut())?;
5273 }
5274 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
5275 self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
5276 }
5277 Expr::Like { expr, pattern, .. } => {
5278 self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
5279 self.resolve_correlated_in_expr(pattern, row, ctx, cancel, memo.as_deref_mut())?;
5280 }
5281 Expr::FunctionCall { args, .. } => {
5282 for a in args {
5283 self.resolve_correlated_in_expr(a, row, ctx, cancel, memo.as_deref_mut())?;
5284 }
5285 }
5286 Expr::Extract { source, .. } => {
5287 self.resolve_correlated_in_expr(source, row, ctx, cancel, memo.as_deref_mut())?;
5288 }
5289 Expr::WindowFunction { .. } | Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {}
5290 }
5291 Ok(())
5292 }
5293
5294 fn subquery_replacement(
5295 &self,
5296 e: &Expr,
5297 cancel: CancelToken<'_>,
5298 ) -> Result<Option<Expr>, EngineError> {
5299 match e {
5300 Expr::ScalarSubquery(inner) => {
5301 let mut s = (**inner).clone();
5302 self.resolve_select_subqueries(&mut s, cancel)?;
5305 let r = match self.exec_bare_select_cancel(&s, cancel) {
5306 Ok(r) => r,
5307 Err(e) if is_correlation_error(&e) => return Ok(None),
5308 Err(e) => return Err(e),
5309 };
5310 let QueryResult::Rows { rows, .. } = r else {
5311 return Err(EngineError::Unsupported(
5312 "scalar subquery: inner statement did not return rows".into(),
5313 ));
5314 };
5315 let value = match rows.as_slice() {
5316 [] => Value::Null,
5317 [row] => row.values.first().cloned().unwrap_or(Value::Null),
5318 _ => {
5319 return Err(EngineError::Unsupported(alloc::format!(
5320 "scalar subquery returned {} rows; expected 0 or 1",
5321 rows.len()
5322 )));
5323 }
5324 };
5325 Ok(Some(value_to_literal_expr(value)?))
5326 }
5327 Expr::Exists { subquery, negated } => {
5328 let mut s = (**subquery).clone();
5329 self.resolve_select_subqueries(&mut s, cancel)?;
5330 let r = match self.exec_bare_select_cancel(&s, cancel) {
5331 Ok(r) => r,
5332 Err(e) if is_correlation_error(&e) => return Ok(None),
5333 Err(e) => return Err(e),
5334 };
5335 let exists = match r {
5336 QueryResult::Rows { rows, .. } => !rows.is_empty(),
5337 QueryResult::CommandOk { .. } => false,
5338 };
5339 let bit = if *negated { !exists } else { exists };
5340 Ok(Some(Expr::Literal(Literal::Bool(bit))))
5341 }
5342 Expr::InSubquery {
5343 expr,
5344 subquery,
5345 negated,
5346 } => {
5347 let mut s = (**subquery).clone();
5348 self.resolve_select_subqueries(&mut s, cancel)?;
5349 let r = match self.exec_bare_select_cancel(&s, cancel) {
5350 Ok(r) => r,
5351 Err(e) if is_correlation_error(&e) => return Ok(None),
5352 Err(e) => return Err(e),
5353 };
5354 let QueryResult::Rows { columns, rows, .. } = r else {
5355 return Err(EngineError::Unsupported(
5356 "IN-subquery: inner statement did not return rows".into(),
5357 ));
5358 };
5359 if columns.len() != 1 {
5360 return Err(EngineError::Unsupported(alloc::format!(
5361 "IN-subquery must project exactly one column; got {}",
5362 columns.len()
5363 )));
5364 }
5365 let mut acc: Option<Expr> = None;
5368 for row in rows {
5369 let v = row.values.into_iter().next().unwrap_or(Value::Null);
5370 let lit = value_to_literal_expr(v)?;
5371 let cmp = Expr::Binary {
5372 lhs: expr.clone(),
5373 op: BinOp::Eq,
5374 rhs: Box::new(lit),
5375 };
5376 acc = Some(match acc {
5377 None => cmp,
5378 Some(prev) => Expr::Binary {
5379 lhs: Box::new(prev),
5380 op: BinOp::Or,
5381 rhs: Box::new(cmp),
5382 },
5383 });
5384 }
5385 let combined = acc.unwrap_or(Expr::Literal(Literal::Bool(false)));
5386 let final_expr = if *negated {
5387 Expr::Unary {
5388 op: UnOp::Not,
5389 expr: Box::new(combined),
5390 }
5391 } else {
5392 combined
5393 };
5394 Ok(Some(final_expr))
5395 }
5396 _ => Ok(None),
5397 }
5398 }
5399}
5400
5401fn select_refers_to(stmt: &SelectStatement, target: &str) -> bool {
5413 if let Some(from) = &stmt.from
5414 && from_refers_to(from, target)
5415 {
5416 return true;
5417 }
5418 for (_, peer) in &stmt.unions {
5419 if select_refers_to(peer, target) {
5420 return true;
5421 }
5422 }
5423 for item in &stmt.items {
5424 if let SelectItem::Expr { expr, .. } = item
5425 && expr_refers_to(expr, target)
5426 {
5427 return true;
5428 }
5429 }
5430 if let Some(w) = &stmt.where_
5431 && expr_refers_to(w, target)
5432 {
5433 return true;
5434 }
5435 false
5436}
5437
5438fn from_refers_to(from: &FromClause, target: &str) -> bool {
5439 if from.primary.name.eq_ignore_ascii_case(target) {
5440 return true;
5441 }
5442 from.joins
5443 .iter()
5444 .any(|j| j.table.name.eq_ignore_ascii_case(target))
5445}
5446
5447fn expr_refers_to(e: &Expr, target: &str) -> bool {
5448 match e {
5449 Expr::ScalarSubquery(s) => select_refers_to(s, target),
5450 Expr::Exists { subquery, .. } | Expr::InSubquery { subquery, .. } => {
5451 select_refers_to(subquery, target)
5452 }
5453 Expr::Binary { lhs, rhs, .. } => expr_refers_to(lhs, target) || expr_refers_to(rhs, target),
5454 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
5455 expr_refers_to(expr, target)
5456 }
5457 Expr::Like { expr, pattern, .. } => {
5458 expr_refers_to(expr, target) || expr_refers_to(pattern, target)
5459 }
5460 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refers_to(a, target)),
5461 Expr::Extract { source, .. } => expr_refers_to(source, target),
5462 Expr::WindowFunction {
5463 args,
5464 partition_by,
5465 order_by,
5466 ..
5467 } => {
5468 args.iter().any(|a| expr_refers_to(a, target))
5469 || partition_by.iter().any(|p| expr_refers_to(p, target))
5470 || order_by.iter().any(|(o, _)| expr_refers_to(o, target))
5471 }
5472 Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => false,
5473 }
5474}
5475
5476fn infer_column_types(columns: &[ColumnSchema], rows: &[Row]) -> Vec<ColumnSchema> {
5482 let mut out = columns.to_vec();
5483 for (col_idx, col) in out.iter_mut().enumerate() {
5484 if col.ty != DataType::Text {
5485 continue;
5486 }
5487 let mut inferred: Option<DataType> = None;
5488 let mut all_null = true;
5489 for row in rows {
5490 let Some(v) = row.values.get(col_idx) else {
5491 continue;
5492 };
5493 let ty = match v {
5494 Value::Null => continue,
5495 Value::SmallInt(_) => DataType::SmallInt,
5496 Value::Int(_) => DataType::Int,
5497 Value::BigInt(_) => DataType::BigInt,
5498 Value::Float(_) => DataType::Float,
5499 Value::Bool(_) => DataType::Bool,
5500 Value::Vector(_) => DataType::Vector {
5501 dim: 0,
5502 encoding: VecEncoding::F32,
5503 },
5504 _ => DataType::Text,
5505 };
5506 all_null = false;
5507 inferred = Some(match inferred {
5508 None => ty,
5509 Some(prev) if prev == ty => prev,
5510 Some(_) => DataType::Text,
5511 });
5512 }
5513 if let Some(t) = inferred {
5514 col.ty = t;
5515 col.nullable = true;
5516 } else if all_null {
5517 col.nullable = true;
5518 }
5519 }
5520 out
5521}
5522
5523#[allow(clippy::too_many_lines, clippy::format_push_string)]
5528fn build_index_suggestions(stmt: &SelectStatement, engine: &Engine) -> Vec<String> {
5545 use alloc::collections::BTreeSet;
5546 let mut seen: BTreeSet<(String, String)> = BTreeSet::new();
5547 let mut out: Vec<String> = Vec::new();
5548 let cat = engine.active_catalog();
5549 let Some(from) = &stmt.from else {
5553 return out;
5554 };
5555 let mut tables: Vec<String> = Vec::new();
5556 tables.push(from.primary.name.clone());
5557 for j in &from.joins {
5558 tables.push(j.table.name.clone());
5559 }
5560 let mut col_refs: Vec<spg_sql::ast::ColumnName> = Vec::new();
5563 if let Some(w) = &stmt.where_ {
5564 collect_column_refs(w, &mut col_refs);
5565 }
5566 for j in &from.joins {
5567 if let Some(on) = &j.on {
5568 collect_column_refs(on, &mut col_refs);
5569 }
5570 }
5571 for cn in &col_refs {
5572 let owner: Option<String> = if let Some(q) = &cn.qualifier {
5575 tables.iter().find(|t| t == &q).cloned()
5576 } else {
5577 tables.iter().find_map(|t| {
5578 cat.get(t).and_then(|tbl| {
5579 if tbl.schema().column_position(&cn.name).is_some() {
5580 Some(t.clone())
5581 } else {
5582 None
5583 }
5584 })
5585 })
5586 };
5587 let Some(owner) = owner else {
5588 continue;
5589 };
5590 let Some(tbl) = cat.get(&owner) else {
5591 continue;
5592 };
5593 let Some(col_pos) = tbl.schema().column_position(&cn.name) else {
5594 continue;
5595 };
5596 let already_indexed = tbl.indices().iter().any(|i| {
5599 matches!(i.kind, spg_storage::IndexKind::BTree(_))
5600 && i.column_position == col_pos
5601 && i.expression.is_none()
5602 && i.partial_predicate.is_none()
5603 });
5604 if already_indexed {
5605 continue;
5606 }
5607 if seen.insert((owner.clone(), cn.name.clone())) {
5608 out.push(alloc::format!(
5609 "SUGGEST: CREATE INDEX ix_{}_{} ON {} ({})",
5610 owner,
5611 cn.name,
5612 owner,
5613 cn.name
5614 ));
5615 }
5616 }
5617 out
5618}
5619
5620fn collect_column_refs(expr: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
5623 match expr {
5624 Expr::Column(cn) => out.push(cn.clone()),
5625 Expr::FunctionCall { args, .. } => {
5626 for a in args {
5627 collect_column_refs(a, out);
5628 }
5629 }
5630 Expr::Binary { lhs, rhs, .. } => {
5631 collect_column_refs(lhs, out);
5632 collect_column_refs(rhs, out);
5633 }
5634 Expr::Unary { expr: e, .. } => collect_column_refs(e, out),
5635 _ => {}
5636 }
5637}
5638
5639fn annotate_explain_lines(lines: &mut [String], total_rows: usize, engine: &Engine) {
5640 let catalog = engine.active_catalog();
5641 let cold_ids = catalog.cold_segment_ids_global();
5642 let any_cold = !cold_ids.is_empty();
5643 let cold_ids_repr = if any_cold {
5644 let mut s = alloc::string::String::from("[");
5645 for (i, id) in cold_ids.iter().enumerate() {
5646 if i > 0 {
5647 s.push(',');
5648 }
5649 s.push_str(&alloc::format!("{id}"));
5650 }
5651 s.push(']');
5652 s
5653 } else {
5654 alloc::string::String::new()
5655 };
5656 for (idx, line) in lines.iter_mut().enumerate() {
5657 let trimmed = line.trim_start();
5658 let is_top_level = idx == 0;
5659 if is_top_level {
5660 line.push_str(&alloc::format!(" (rows={total_rows})"));
5661 continue;
5662 }
5663 if let Some(rest) = trimmed.strip_prefix("From: ") {
5664 let (name, scan_kind) = match rest.split_once(" [") {
5665 Some((n, k)) => (n.trim(), k.trim_end_matches(']')),
5666 None => (rest.trim(), ""),
5667 };
5668 let bare = name.split_whitespace().next().unwrap_or(name);
5669 let hot = catalog.get(bare).map(|t| t.rows().len());
5670 let annot = match (hot, scan_kind) {
5675 (Some(h), "full scan") => {
5676 let mut s = alloc::format!(" (hot_rows={h}");
5677 if any_cold {
5678 s.push_str(&alloc::format!(
5679 ", cold_tier=present, cold_segments={cold_ids_repr}"
5680 ));
5681 }
5682 s.push(')');
5683 s
5684 }
5685 (Some(h), "index seek") => {
5686 let mut s = alloc::format!(" (hot_rows≤{h}");
5687 if any_cold {
5688 s.push_str(&alloc::format!(
5689 ", cold_tier=present, cold_segments={cold_ids_repr}"
5690 ));
5691 }
5692 s.push(')');
5693 s
5694 }
5695 _ => " (rows=—)".to_string(),
5696 };
5697 line.push_str(&annot);
5698 continue;
5699 }
5700 line.push_str(" (rows=—)");
5702 }
5703}
5704
5705fn explain_select(stmt: &SelectStatement, engine: &Engine, depth: usize, out: &mut Vec<String>) {
5706 let pad = " ".repeat(depth);
5707 let top = if !stmt.ctes.is_empty() {
5709 if stmt.ctes.iter().any(|c| c.recursive) {
5710 "CTEScan (WITH RECURSIVE)"
5711 } else {
5712 "CTEScan (WITH)"
5713 }
5714 } else if !stmt.unions.is_empty() {
5715 "UnionScan"
5716 } else if select_has_window(stmt) {
5717 "WindowAgg"
5718 } else if aggregate::uses_aggregate(stmt) {
5719 "Aggregate"
5720 } else if stmt.distinct {
5721 "Distinct"
5722 } else if stmt.from.is_some() {
5723 "TableScan"
5724 } else {
5725 "Result"
5726 };
5727 out.push(alloc::format!("{pad}{top}"));
5728 let child = " ".repeat(depth + 1);
5729 for cte in &stmt.ctes {
5731 let head = if cte.recursive {
5732 alloc::format!("{child}CTE (recursive): {}", cte.name)
5733 } else {
5734 alloc::format!("{child}CTE: {}", cte.name)
5735 };
5736 out.push(head);
5737 explain_select(&cte.body, engine, depth + 2, out);
5738 }
5739 if let Some(from) = &stmt.from {
5741 let mut tag = alloc::format!("{child}From: {}", from.primary.name);
5742 if let Some(alias) = &from.primary.alias {
5743 tag.push_str(&alloc::format!(" AS {alias}"));
5744 }
5745 if let Some(w) = &stmt.where_
5748 && let Some(table) = engine.active_catalog().get(&from.primary.name)
5749 {
5750 let alias = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
5751 let cols = &table.schema().columns;
5752 if try_index_seek(w, cols, engine.active_catalog(), table, alias).is_some() {
5753 tag.push_str(" [index seek]");
5754 } else {
5755 tag.push_str(" [full scan]");
5756 }
5757 } else {
5758 tag.push_str(" [full scan]");
5759 }
5760 out.push(tag);
5761 for j in &from.joins {
5762 let kind = match j.kind {
5763 spg_sql::ast::JoinKind::Inner => "INNER JOIN",
5764 spg_sql::ast::JoinKind::Left => "LEFT JOIN",
5765 spg_sql::ast::JoinKind::Cross => "CROSS JOIN",
5766 };
5767 let mut s = alloc::format!("{child}{kind}: {}", j.table.name);
5768 if let Some(alias) = &j.table.alias {
5769 s.push_str(&alloc::format!(" AS {alias}"));
5770 }
5771 if j.on.is_some() {
5772 s.push_str(" (ON …)");
5773 }
5774 out.push(s);
5775 }
5776 }
5777 if let Some(w) = &stmt.where_ {
5779 let mut s = alloc::format!("{child}Filter: {w}");
5780 if expr_has_subquery(w) {
5781 s.push_str(" [subquery]");
5782 }
5783 out.push(s);
5784 }
5785 if let Some(gs) = &stmt.group_by {
5786 let mut parts = Vec::new();
5787 for g in gs {
5788 parts.push(alloc::format!("{g}"));
5789 }
5790 out.push(alloc::format!("{child}GroupBy: {}", parts.join(", ")));
5791 }
5792 if let Some(h) = &stmt.having {
5793 out.push(alloc::format!("{child}Having: {h}"));
5794 }
5795 for o in &stmt.order_by {
5796 let dir = if o.desc { "DESC" } else { "ASC" };
5797 out.push(alloc::format!("{child}OrderBy: {} {dir}", o.expr));
5798 }
5799 if let Some(lim) = stmt.limit {
5800 out.push(alloc::format!("{child}Limit: {lim}"));
5801 }
5802 if let Some(off) = stmt.offset {
5803 out.push(alloc::format!("{child}Offset: {off}"));
5804 }
5805 if stmt
5807 .items
5808 .iter()
5809 .any(|it| matches!(it, SelectItem::Wildcard))
5810 {
5811 out.push(alloc::format!("{child}Project: *"));
5812 } else {
5813 out.push(alloc::format!(
5814 "{child}Project: {} item(s)",
5815 stmt.items.len()
5816 ));
5817 }
5818 for (kind, peer) in &stmt.unions {
5820 let label = match kind {
5821 UnionKind::All => "UNION ALL",
5822 UnionKind::Distinct => "UNION",
5823 };
5824 out.push(alloc::format!("{child}{label}"));
5825 explain_select(peer, engine, depth + 2, out);
5826 }
5827}
5828
5829fn is_correlation_error(e: &EngineError) -> bool {
5834 matches!(
5835 e,
5836 EngineError::Eval(
5837 eval::EvalError::ColumnNotFound { .. } | eval::EvalError::UnknownQualifier { .. }
5838 )
5839 )
5840}
5841
5842fn substitute_outer_columns(stmt: &mut SelectStatement, row: &Row, ctx: &EvalContext<'_>) {
5850 let Some(outer_alias) = ctx.table_alias else {
5851 return;
5852 };
5853 substitute_in_select(stmt, row, ctx, outer_alias);
5854}
5855
5856fn substitute_in_select(
5857 stmt: &mut SelectStatement,
5858 row: &Row,
5859 ctx: &EvalContext<'_>,
5860 outer_alias: &str,
5861) {
5862 for item in &mut stmt.items {
5863 if let SelectItem::Expr { expr, .. } = item {
5864 substitute_in_expr(expr, row, ctx, outer_alias);
5865 }
5866 }
5867 if let Some(w) = &mut stmt.where_ {
5868 substitute_in_expr(w, row, ctx, outer_alias);
5869 }
5870 if let Some(gs) = &mut stmt.group_by {
5871 for g in gs {
5872 substitute_in_expr(g, row, ctx, outer_alias);
5873 }
5874 }
5875 if let Some(h) = &mut stmt.having {
5876 substitute_in_expr(h, row, ctx, outer_alias);
5877 }
5878 for o in &mut stmt.order_by {
5879 substitute_in_expr(&mut o.expr, row, ctx, outer_alias);
5880 }
5881 for (_, peer) in &mut stmt.unions {
5882 substitute_in_select(peer, row, ctx, outer_alias);
5883 }
5884}
5885
5886fn substitute_in_expr(e: &mut Expr, row: &Row, ctx: &EvalContext<'_>, outer_alias: &str) {
5887 if let Expr::Column(c) = e
5888 && let Some(qual) = &c.qualifier
5889 && qual.eq_ignore_ascii_case(outer_alias)
5890 {
5891 if let Some(idx) = ctx
5893 .columns
5894 .iter()
5895 .position(|sc| sc.name.eq_ignore_ascii_case(&c.name))
5896 {
5897 let v = row.values.get(idx).cloned().unwrap_or(Value::Null);
5898 if let Ok(lit) = value_to_literal_expr(v) {
5899 *e = lit;
5900 return;
5901 }
5902 }
5903 }
5904 match e {
5905 Expr::Binary { lhs, rhs, .. } => {
5906 substitute_in_expr(lhs, row, ctx, outer_alias);
5907 substitute_in_expr(rhs, row, ctx, outer_alias);
5908 }
5909 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
5910 substitute_in_expr(expr, row, ctx, outer_alias);
5911 }
5912 Expr::Like { expr, pattern, .. } => {
5913 substitute_in_expr(expr, row, ctx, outer_alias);
5914 substitute_in_expr(pattern, row, ctx, outer_alias);
5915 }
5916 Expr::FunctionCall { args, .. } => {
5917 for a in args {
5918 substitute_in_expr(a, row, ctx, outer_alias);
5919 }
5920 }
5921 Expr::Extract { source, .. } => substitute_in_expr(source, row, ctx, outer_alias),
5922 Expr::WindowFunction {
5923 args,
5924 partition_by,
5925 order_by,
5926 ..
5927 } => {
5928 for a in args {
5929 substitute_in_expr(a, row, ctx, outer_alias);
5930 }
5931 for p in partition_by {
5932 substitute_in_expr(p, row, ctx, outer_alias);
5933 }
5934 for (o, _) in order_by {
5935 substitute_in_expr(o, row, ctx, outer_alias);
5936 }
5937 }
5938 Expr::ScalarSubquery(s) => substitute_in_select(s, row, ctx, outer_alias),
5939 Expr::Exists { subquery, .. } | Expr::InSubquery { subquery, .. } => {
5940 substitute_in_select(subquery, row, ctx, outer_alias);
5941 }
5942 Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {}
5943 }
5944}
5945
5946fn encode_row_key(row: &Row) -> Vec<u8> {
5950 let mut out = Vec::new();
5951 for v in &row.values {
5952 let s = alloc::format!("{v:?}|");
5953 out.extend_from_slice(s.as_bytes());
5954 }
5955 out
5956}
5957
5958fn select_has_window(stmt: &SelectStatement) -> bool {
5959 for item in &stmt.items {
5960 if let SelectItem::Expr { expr, .. } = item
5961 && expr_has_window(expr)
5962 {
5963 return true;
5964 }
5965 }
5966 false
5967}
5968
5969fn expr_has_window(e: &Expr) -> bool {
5970 match e {
5971 Expr::WindowFunction { .. } => true,
5972 Expr::Binary { lhs, rhs, .. } => expr_has_window(lhs) || expr_has_window(rhs),
5973 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
5974 expr_has_window(expr)
5975 }
5976 Expr::FunctionCall { args, .. } => args.iter().any(expr_has_window),
5977 Expr::Like { expr, pattern, .. } => expr_has_window(expr) || expr_has_window(pattern),
5978 Expr::Extract { source, .. } => expr_has_window(source),
5979 Expr::ScalarSubquery(_)
5980 | Expr::Exists { .. }
5981 | Expr::InSubquery { .. }
5982 | Expr::Literal(_)
5983 | Expr::Placeholder(_)
5984 | Expr::Column(_) => false,
5985 }
5986}
5987
5988fn collect_window_nodes(e: &Expr, out: &mut Vec<Expr>) {
5989 if let Expr::WindowFunction { .. } = e {
5990 if !out.iter().any(|x| x == e) {
5995 out.push(e.clone());
5996 }
5997 return;
5998 }
5999 match e {
6000 Expr::WindowFunction { .. } => unreachable!(),
6002 Expr::Binary { lhs, rhs, .. } => {
6003 collect_window_nodes(lhs, out);
6004 collect_window_nodes(rhs, out);
6005 }
6006 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6007 collect_window_nodes(expr, out);
6008 }
6009 Expr::FunctionCall { args, .. } => {
6010 for a in args {
6011 collect_window_nodes(a, out);
6012 }
6013 }
6014 Expr::Like { expr, pattern, .. } => {
6015 collect_window_nodes(expr, out);
6016 collect_window_nodes(pattern, out);
6017 }
6018 Expr::Extract { source, .. } => collect_window_nodes(source, out),
6019 _ => {}
6020 }
6021}
6022
6023fn rewrite_window_to_columns(e: &mut Expr, window_nodes: &[Expr]) {
6024 if let Expr::WindowFunction { .. } = e
6025 && let Some(idx) = window_nodes.iter().position(|w| w == e)
6026 {
6027 *e = Expr::Column(spg_sql::ast::ColumnName {
6028 qualifier: None,
6029 name: alloc::format!("__win_{idx}"),
6030 });
6031 return;
6032 }
6033 match e {
6034 Expr::Binary { lhs, rhs, .. } => {
6035 rewrite_window_to_columns(lhs, window_nodes);
6036 rewrite_window_to_columns(rhs, window_nodes);
6037 }
6038 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6039 rewrite_window_to_columns(expr, window_nodes);
6040 }
6041 Expr::FunctionCall { args, .. } => {
6042 for a in args {
6043 rewrite_window_to_columns(a, window_nodes);
6044 }
6045 }
6046 Expr::Like { expr, pattern, .. } => {
6047 rewrite_window_to_columns(expr, window_nodes);
6048 rewrite_window_to_columns(pattern, window_nodes);
6049 }
6050 Expr::Extract { source, .. } => rewrite_window_to_columns(source, window_nodes),
6051 _ => {}
6052 }
6053}
6054
6055fn partition_key_cmp(a: &[Value], b: &[Value]) -> core::cmp::Ordering {
6059 for (x, y) in a.iter().zip(b.iter()) {
6060 let c = value_cmp(x, y);
6061 if c != core::cmp::Ordering::Equal {
6062 return c;
6063 }
6064 }
6065 a.len().cmp(&b.len())
6066}
6067
6068fn order_key_cmp(a: &[(Value, bool)], b: &[(Value, bool)]) -> core::cmp::Ordering {
6069 for ((va, desc), (vb, _)) in a.iter().zip(b.iter()) {
6070 let c = value_cmp(va, vb);
6071 let c = if *desc { c.reverse() } else { c };
6072 if c != core::cmp::Ordering::Equal {
6073 return c;
6074 }
6075 }
6076 a.len().cmp(&b.len())
6077}
6078
6079#[allow(clippy::match_same_arms)] fn value_cmp(a: &Value, b: &Value) -> core::cmp::Ordering {
6081 use core::cmp::Ordering;
6082 match (a, b) {
6083 (Value::Null, Value::Null) => Ordering::Equal,
6084 (Value::Null, _) => Ordering::Less,
6085 (_, Value::Null) => Ordering::Greater,
6086 (Value::Int(x), Value::Int(y)) => x.cmp(y),
6087 (Value::BigInt(x), Value::BigInt(y)) => x.cmp(y),
6088 (Value::SmallInt(x), Value::SmallInt(y)) => x.cmp(y),
6089 (Value::Text(x), Value::Text(y)) => x.cmp(y),
6090 (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
6091 (Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
6092 (Value::Date(x), Value::Date(y)) => x.cmp(y),
6093 (Value::Timestamp(x), Value::Timestamp(y)) => x.cmp(y),
6094 _ => alloc::format!("{a:?}").cmp(&alloc::format!("{b:?}")),
6097 }
6098}
6099
6100#[allow(
6106 clippy::too_many_arguments,
6107 clippy::cast_possible_truncation,
6108 clippy::cast_possible_wrap,
6109 clippy::cast_precision_loss,
6110 clippy::cast_sign_loss,
6111 clippy::doc_markdown,
6112 clippy::too_many_lines,
6113 clippy::type_complexity,
6114 clippy::match_same_arms
6115)]
6116fn compute_window_partition(
6117 name: &str,
6118 args: &[Expr],
6119 ordered: bool,
6120 frame: Option<&WindowFrame>,
6121 null_treatment: spg_sql::ast::NullTreatment,
6122 slice: &[(Vec<Value>, Vec<(Value, bool)>, usize)],
6123 filtered_rows: &[&Row],
6124 ctx: &EvalContext<'_>,
6125 out_vals: &mut [Value],
6126) -> Result<(), EngineError> {
6127 let ignore_nulls = matches!(null_treatment, spg_sql::ast::NullTreatment::Ignore);
6128 let lower = name.to_ascii_lowercase();
6129 match lower.as_str() {
6130 "row_number" => {
6131 for (rank, (_, _, idx)) in slice.iter().enumerate() {
6132 out_vals[*idx] = Value::BigInt((rank + 1) as i64);
6133 }
6134 Ok(())
6135 }
6136 "rank" => {
6137 let mut prev_key: Option<&[(Value, bool)]> = None;
6138 let mut current_rank: i64 = 1;
6139 for (i, (_, okey, idx)) in slice.iter().enumerate() {
6140 if let Some(p) = prev_key
6141 && order_key_cmp(p, okey) != core::cmp::Ordering::Equal
6142 {
6143 current_rank = (i + 1) as i64;
6144 }
6145 if prev_key.is_none() {
6146 current_rank = 1;
6147 }
6148 out_vals[*idx] = Value::BigInt(current_rank);
6149 prev_key = Some(okey.as_slice());
6150 }
6151 Ok(())
6152 }
6153 "dense_rank" => {
6154 let mut prev_key: Option<&[(Value, bool)]> = None;
6155 let mut current_rank: i64 = 0;
6156 for (_, okey, idx) in slice {
6157 if prev_key.is_none_or(|p| order_key_cmp(p, okey) != core::cmp::Ordering::Equal) {
6158 current_rank += 1;
6159 }
6160 out_vals[*idx] = Value::BigInt(current_rank);
6161 prev_key = Some(okey.as_slice());
6162 }
6163 Ok(())
6164 }
6165 "sum" | "avg" | "min" | "max" | "count" | "count_star" => {
6166 let arg_values: Vec<Value> = if lower == "count_star" || args.is_empty() {
6169 slice.iter().map(|_| Value::Null).collect()
6170 } else {
6171 slice
6172 .iter()
6173 .map(|(_, _, idx)| eval::eval_expr(&args[0], filtered_rows[*idx], ctx))
6174 .collect::<Result<_, _>>()
6175 .map_err(EngineError::Eval)?
6176 };
6177 let eff = effective_frame(frame, ordered)?;
6181 #[allow(clippy::needless_range_loop)]
6182 for i in 0..slice.len() {
6183 let (lo, hi) = frame_bounds_for_row(&eff, i, slice);
6184 let mut sum: f64 = 0.0;
6185 let mut count: i64 = 0;
6186 let mut min_v: Option<f64> = None;
6187 let mut max_v: Option<f64> = None;
6188 let mut row_count: i64 = 0;
6189 if lo <= hi {
6190 for j in lo..=hi {
6191 let v = &arg_values[j];
6192 match lower.as_str() {
6193 "count_star" => row_count += 1,
6194 "count" => {
6195 if !v.is_null() {
6196 count += 1;
6197 }
6198 }
6199 _ => {
6200 if let Some(x) = value_to_f64(v) {
6201 sum += x;
6202 count += 1;
6203 min_v = Some(min_v.map_or(x, |m| m.min(x)));
6204 max_v = Some(max_v.map_or(x, |m| m.max(x)));
6205 }
6206 }
6207 }
6208 }
6209 }
6210 let value = match lower.as_str() {
6211 "count_star" => Value::BigInt(row_count),
6212 "count" => Value::BigInt(count),
6213 "sum" => Value::Float(sum),
6214 "avg" => {
6215 if count == 0 {
6216 Value::Null
6217 } else {
6218 Value::Float(sum / count as f64)
6219 }
6220 }
6221 "min" => min_v.map_or(Value::Null, Value::Float),
6222 "max" => max_v.map_or(Value::Null, Value::Float),
6223 _ => unreachable!(),
6224 };
6225 let (_, _, idx) = &slice[i];
6226 out_vals[*idx] = value;
6227 }
6228 Ok(())
6229 }
6230 "lag" | "lead" => {
6231 if args.is_empty() {
6234 return Err(EngineError::Unsupported(alloc::format!(
6235 "{lower}() requires at least one argument"
6236 )));
6237 }
6238 let offset: i64 = if args.len() >= 2 {
6239 let v = eval::eval_expr(&args[1], filtered_rows[slice[0].2], ctx)
6240 .map_err(EngineError::Eval)?;
6241 match v {
6242 Value::SmallInt(n) => i64::from(n),
6243 Value::Int(n) => i64::from(n),
6244 Value::BigInt(n) => n,
6245 _ => {
6246 return Err(EngineError::Unsupported(alloc::format!(
6247 "{lower}() offset must be integer"
6248 )));
6249 }
6250 }
6251 } else {
6252 1
6253 };
6254 let default: Value = if args.len() >= 3 {
6255 eval::eval_expr(&args[2], filtered_rows[slice[0].2], ctx)
6256 .map_err(EngineError::Eval)?
6257 } else {
6258 Value::Null
6259 };
6260 let values: Vec<Value> = slice
6261 .iter()
6262 .map(|(_, _, idx)| eval::eval_expr(&args[0], filtered_rows[*idx], ctx))
6263 .collect::<Result<_, _>>()
6264 .map_err(EngineError::Eval)?;
6265 let n = slice.len();
6266 for (i, (_, _, idx)) in slice.iter().enumerate() {
6267 let signed_offset = if lower == "lag" { -offset } else { offset };
6268 let v = if ignore_nulls {
6269 let step: i64 = if signed_offset >= 0 { 1 } else { -1 };
6273 let needed: i64 = signed_offset.abs();
6274 if needed == 0 {
6275 values[i].clone()
6276 } else {
6277 let mut j: i64 = i as i64;
6278 let mut hits: i64 = 0;
6279 let mut found: Option<Value> = None;
6280 loop {
6281 j += step;
6282 if j < 0 || j >= n as i64 {
6283 break;
6284 }
6285 #[allow(clippy::cast_sign_loss)]
6286 let v = &values[j as usize];
6287 if !v.is_null() {
6288 hits += 1;
6289 if hits == needed {
6290 found = Some(v.clone());
6291 break;
6292 }
6293 }
6294 }
6295 found.unwrap_or_else(|| default.clone())
6296 }
6297 } else {
6298 let target_signed = i64::try_from(i).unwrap_or(i64::MAX) + signed_offset;
6299 if target_signed < 0
6300 || target_signed >= i64::try_from(n).unwrap_or(i64::MAX)
6301 {
6302 default.clone()
6303 } else {
6304 #[allow(clippy::cast_sign_loss)]
6305 {
6306 values[target_signed as usize].clone()
6307 }
6308 }
6309 };
6310 out_vals[*idx] = v;
6311 }
6312 Ok(())
6313 }
6314 "first_value" | "last_value" | "nth_value" => {
6315 if args.is_empty() {
6316 return Err(EngineError::Unsupported(alloc::format!(
6317 "{lower}() requires at least one argument"
6318 )));
6319 }
6320 let values: Vec<Value> = slice
6321 .iter()
6322 .map(|(_, _, idx)| eval::eval_expr(&args[0], filtered_rows[*idx], ctx))
6323 .collect::<Result<_, _>>()
6324 .map_err(EngineError::Eval)?;
6325 let nth: usize = if lower == "nth_value" {
6326 if args.len() < 2 {
6327 return Err(EngineError::Unsupported(
6328 "nth_value() requires (expr, n)".into(),
6329 ));
6330 }
6331 let v = eval::eval_expr(&args[1], filtered_rows[slice[0].2], ctx)
6332 .map_err(EngineError::Eval)?;
6333 let raw = match v {
6334 Value::SmallInt(n) => i64::from(n),
6335 Value::Int(n) => i64::from(n),
6336 Value::BigInt(n) => n,
6337 _ => {
6338 return Err(EngineError::Unsupported(
6339 "nth_value() n must be integer".into(),
6340 ));
6341 }
6342 };
6343 if raw < 1 {
6344 return Err(EngineError::Unsupported(
6345 "nth_value() n must be >= 1".into(),
6346 ));
6347 }
6348 #[allow(clippy::cast_sign_loss)]
6349 {
6350 raw as usize
6351 }
6352 } else {
6353 0
6354 };
6355 let eff = effective_frame(frame, ordered)?;
6356 for i in 0..slice.len() {
6357 let (lo, hi) = frame_bounds_for_row(&eff, i, slice);
6358 let (_, _, idx) = &slice[i];
6359 let v = if lo > hi {
6360 Value::Null
6361 } else if ignore_nulls && matches!(lower.as_str(), "first_value" | "last_value") {
6362 if lower == "first_value" {
6365 (lo..=hi)
6366 .find_map(|j| {
6367 let v = &values[j];
6368 (!v.is_null()).then(|| v.clone())
6369 })
6370 .unwrap_or(Value::Null)
6371 } else {
6372 (lo..=hi)
6373 .rev()
6374 .find_map(|j| {
6375 let v = &values[j];
6376 (!v.is_null()).then(|| v.clone())
6377 })
6378 .unwrap_or(Value::Null)
6379 }
6380 } else {
6381 match lower.as_str() {
6382 "first_value" => values[lo].clone(),
6383 "last_value" => values[hi].clone(),
6384 "nth_value" => {
6385 let pos = lo + nth - 1;
6386 if pos > hi {
6387 Value::Null
6388 } else {
6389 values[pos].clone()
6390 }
6391 }
6392 _ => unreachable!(),
6393 }
6394 };
6395 out_vals[*idx] = v;
6396 }
6397 Ok(())
6398 }
6399 "ntile" => {
6400 if args.is_empty() {
6401 return Err(EngineError::Unsupported(
6402 "ntile(n) requires an integer argument".into(),
6403 ));
6404 }
6405 let v = eval::eval_expr(&args[0], filtered_rows[slice[0].2], ctx)
6406 .map_err(EngineError::Eval)?;
6407 let bucket_count: i64 = match v {
6408 Value::SmallInt(n) => i64::from(n),
6409 Value::Int(n) => i64::from(n),
6410 Value::BigInt(n) => n,
6411 _ => {
6412 return Err(EngineError::Unsupported(
6413 "ntile() argument must be integer".into(),
6414 ));
6415 }
6416 };
6417 if bucket_count < 1 {
6418 return Err(EngineError::Unsupported(
6419 "ntile() argument must be >= 1".into(),
6420 ));
6421 }
6422 #[allow(clippy::cast_sign_loss)]
6423 let buckets = bucket_count as usize;
6424 let n = slice.len();
6425 let base = n / buckets;
6428 let extras = n % buckets;
6429 let mut bucket: usize = 1;
6430 let mut remaining_in_bucket = if extras > 0 { base + 1 } else { base };
6431 let mut buckets_with_extra_remaining = extras;
6432 for (_, _, idx) in slice {
6433 if remaining_in_bucket == 0 {
6434 bucket += 1;
6435 buckets_with_extra_remaining = buckets_with_extra_remaining.saturating_sub(1);
6436 remaining_in_bucket = if buckets_with_extra_remaining > 0 {
6437 base + 1
6438 } else {
6439 base
6440 };
6441 if remaining_in_bucket == 0 {
6444 remaining_in_bucket = 1;
6445 }
6446 }
6447 out_vals[*idx] = Value::BigInt(i64::try_from(bucket).unwrap_or(i64::MAX));
6448 remaining_in_bucket -= 1;
6449 }
6450 Ok(())
6451 }
6452 "percent_rank" => {
6453 let n = slice.len();
6456 let mut prev_key: Option<&[(Value, bool)]> = None;
6457 let mut current_rank: i64 = 1;
6458 for (i, (_, okey, idx)) in slice.iter().enumerate() {
6459 if let Some(p) = prev_key
6460 && order_key_cmp(p, okey) != core::cmp::Ordering::Equal
6461 {
6462 current_rank = i64::try_from(i + 1).unwrap_or(i64::MAX);
6463 }
6464 if prev_key.is_none() {
6465 current_rank = 1;
6466 }
6467 #[allow(clippy::cast_precision_loss)]
6468 let pr = if n <= 1 {
6469 0.0
6470 } else {
6471 (current_rank - 1) as f64 / (n - 1) as f64
6472 };
6473 out_vals[*idx] = Value::Float(pr);
6474 prev_key = Some(okey.as_slice());
6475 }
6476 Ok(())
6477 }
6478 "cume_dist" => {
6479 let n = slice.len();
6481 for i in 0..slice.len() {
6483 let peer_end = peer_group_end(slice, i);
6484 #[allow(clippy::cast_precision_loss)]
6485 let cd = (peer_end + 1) as f64 / n as f64;
6486 let (_, _, idx) = &slice[i];
6487 out_vals[*idx] = Value::Float(cd);
6488 }
6489 Ok(())
6490 }
6491 other => Err(EngineError::Unsupported(alloc::format!(
6492 "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)"
6493 ))),
6494 }
6495}
6496
6497fn effective_frame(
6504 frame: Option<&WindowFrame>,
6505 ordered: bool,
6506) -> Result<(FrameKind, FrameBound, FrameBound), EngineError> {
6507 match frame {
6508 None => {
6509 if ordered {
6510 Ok((
6511 FrameKind::Range,
6512 FrameBound::UnboundedPreceding,
6513 FrameBound::CurrentRow,
6514 ))
6515 } else {
6516 Ok((
6517 FrameKind::Rows,
6518 FrameBound::UnboundedPreceding,
6519 FrameBound::UnboundedFollowing,
6520 ))
6521 }
6522 }
6523 Some(fr) => {
6524 let end = fr.end.clone().unwrap_or(FrameBound::CurrentRow);
6525 if matches!(fr.start, FrameBound::UnboundedFollowing)
6527 || matches!(end, FrameBound::UnboundedPreceding)
6528 {
6529 return Err(EngineError::Unsupported(alloc::format!(
6530 "invalid frame: start={:?} end={:?}",
6531 fr.start,
6532 end
6533 )));
6534 }
6535 if fr.kind == FrameKind::Range
6540 && (matches!(
6541 fr.start,
6542 FrameBound::OffsetPreceding(_) | FrameBound::OffsetFollowing(_)
6543 ) || matches!(
6544 end,
6545 FrameBound::OffsetPreceding(_) | FrameBound::OffsetFollowing(_)
6546 ))
6547 {
6548 return Err(EngineError::Unsupported(
6549 "RANGE with explicit offset bounds is not supported (v4.20: only UNBOUNDED / CURRENT ROW for RANGE)".into(),
6550 ));
6551 }
6552 Ok((fr.kind, fr.start.clone(), end))
6553 }
6554 }
6555}
6556
6557#[allow(clippy::type_complexity)]
6561fn frame_bounds_for_row(
6562 eff: &(FrameKind, FrameBound, FrameBound),
6563 i: usize,
6564 slice: &[(Vec<Value>, Vec<(Value, bool)>, usize)],
6565) -> (usize, usize) {
6566 let (kind, start, end) = eff;
6567 let n = slice.len();
6568 let last = n.saturating_sub(1);
6569 let (mut lo, mut hi) = match kind {
6570 FrameKind::Rows => {
6571 let lo = match start {
6572 FrameBound::UnboundedPreceding => 0,
6573 FrameBound::OffsetPreceding(k) => {
6574 let k = usize::try_from(*k).unwrap_or(usize::MAX);
6575 i.saturating_sub(k)
6576 }
6577 FrameBound::CurrentRow => i,
6578 FrameBound::OffsetFollowing(k) => {
6579 let k = usize::try_from(*k).unwrap_or(usize::MAX);
6580 i.saturating_add(k).min(last)
6581 }
6582 FrameBound::UnboundedFollowing => last,
6583 };
6584 let hi = match end {
6585 FrameBound::UnboundedPreceding => 0,
6586 FrameBound::OffsetPreceding(k) => {
6587 let k = usize::try_from(*k).unwrap_or(usize::MAX);
6588 i.saturating_sub(k)
6589 }
6590 FrameBound::CurrentRow => i,
6591 FrameBound::OffsetFollowing(k) => {
6592 let k = usize::try_from(*k).unwrap_or(usize::MAX);
6593 i.saturating_add(k).min(last)
6594 }
6595 FrameBound::UnboundedFollowing => last,
6596 };
6597 (lo, hi)
6598 }
6599 FrameKind::Range => {
6600 let lo = match start {
6606 FrameBound::UnboundedPreceding => 0,
6607 FrameBound::CurrentRow => peer_group_start(slice, i),
6608 FrameBound::UnboundedFollowing => last,
6609 _ => unreachable!("offset bounds rejected for RANGE"),
6610 };
6611 let hi = match end {
6612 FrameBound::UnboundedPreceding => 0,
6613 FrameBound::CurrentRow => peer_group_end(slice, i),
6614 FrameBound::UnboundedFollowing => last,
6615 _ => unreachable!("offset bounds rejected for RANGE"),
6616 };
6617 (lo, hi)
6618 }
6619 };
6620 if hi >= n {
6621 hi = last;
6622 }
6623 if lo >= n {
6624 lo = last;
6625 }
6626 (lo, hi)
6627}
6628
6629#[allow(clippy::type_complexity)]
6633fn peer_group_start(slice: &[(Vec<Value>, Vec<(Value, bool)>, usize)], i: usize) -> usize {
6634 let key = &slice[i].1;
6635 let mut j = i;
6636 while j > 0 && order_key_cmp(&slice[j - 1].1, key) == core::cmp::Ordering::Equal {
6637 j -= 1;
6638 }
6639 j
6640}
6641
6642#[allow(clippy::type_complexity)]
6645fn peer_group_end(slice: &[(Vec<Value>, Vec<(Value, bool)>, usize)], i: usize) -> usize {
6646 let key = &slice[i].1;
6647 let mut j = i;
6648 while j + 1 < slice.len() && order_key_cmp(&slice[j + 1].1, key) == core::cmp::Ordering::Equal {
6649 j += 1;
6650 }
6651 j
6652}
6653
6654fn value_to_f64(v: &Value) -> Option<f64> {
6655 match v {
6656 Value::SmallInt(n) => Some(f64::from(*n)),
6657 Value::Int(n) => Some(f64::from(*n)),
6658 #[allow(clippy::cast_precision_loss)]
6659 Value::BigInt(n) => Some(*n as f64),
6660 Value::Float(x) => Some(*x),
6661 _ => None,
6662 }
6663}
6664
6665fn expr_tree_has_subquery(stmt: &SelectStatement) -> bool {
6669 let mut any = false;
6670 for item in &stmt.items {
6671 if let SelectItem::Expr { expr, .. } = item {
6672 any = any || expr_has_subquery(expr);
6673 }
6674 }
6675 if let Some(w) = &stmt.where_ {
6676 any = any || expr_has_subquery(w);
6677 }
6678 if let Some(h) = &stmt.having {
6679 any = any || expr_has_subquery(h);
6680 }
6681 for o in &stmt.order_by {
6682 any = any || expr_has_subquery(&o.expr);
6683 }
6684 for (_, peer) in &stmt.unions {
6685 any = any || expr_tree_has_subquery(peer);
6686 }
6687 any
6688}
6689
6690fn expr_has_subquery(e: &Expr) -> bool {
6691 match e {
6692 Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => true,
6693 Expr::Binary { lhs, rhs, .. } => expr_has_subquery(lhs) || expr_has_subquery(rhs),
6694 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6695 expr_has_subquery(expr)
6696 }
6697 Expr::FunctionCall { args, .. } => args.iter().any(expr_has_subquery),
6698 Expr::Like { expr, pattern, .. } => expr_has_subquery(expr) || expr_has_subquery(pattern),
6699 Expr::Extract { source, .. } => expr_has_subquery(source),
6700 Expr::WindowFunction {
6701 args,
6702 partition_by,
6703 order_by,
6704 ..
6705 } => {
6706 args.iter().any(expr_has_subquery)
6707 || partition_by.iter().any(expr_has_subquery)
6708 || order_by.iter().any(|(e, _)| expr_has_subquery(e))
6709 }
6710 Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => false,
6711 }
6712}
6713
6714fn value_to_literal_expr(v: Value) -> Result<Expr, EngineError> {
6721 let lit = match v {
6722 Value::Null => Literal::Null,
6723 Value::SmallInt(n) => Literal::Integer(i64::from(n)),
6724 Value::Int(n) => Literal::Integer(i64::from(n)),
6725 Value::BigInt(n) => Literal::Integer(n),
6726 Value::Float(x) => Literal::Float(x),
6727 Value::Text(s) | Value::Json(s) => Literal::String(s),
6728 Value::Bool(b) => Literal::Bool(b),
6729 other => {
6730 return Err(EngineError::Unsupported(alloc::format!(
6731 "subquery result type {:?} not yet materialisable; cast to text or integer in the inner SELECT",
6732 other.data_type()
6733 )));
6734 }
6735 };
6736 Ok(Expr::Literal(lit))
6737}
6738
6739fn substitute_placeholders(stmt: &mut Statement, params: &[Value]) -> Result<(), EngineError> {
6750 match stmt {
6751 Statement::Select(s) => substitute_select(s, params)?,
6752 Statement::Insert(ins) => {
6753 for row in &mut ins.rows {
6754 for e in row {
6755 substitute_expr(e, params)?;
6756 }
6757 }
6758 }
6759 Statement::Update(u) => {
6760 for (_, e) in &mut u.assignments {
6761 substitute_expr(e, params)?;
6762 }
6763 if let Some(w) = &mut u.where_ {
6764 substitute_expr(w, params)?;
6765 }
6766 }
6767 Statement::Delete(d) => {
6768 if let Some(w) = &mut d.where_ {
6769 substitute_expr(w, params)?;
6770 }
6771 }
6772 Statement::Explain(e) => substitute_select(&mut e.inner, params)?,
6773 _ => {}
6776 }
6777 Ok(())
6778}
6779
6780fn substitute_select(
6781 s: &mut SelectStatement,
6782 params: &[Value],
6783) -> Result<(), EngineError> {
6784 for item in &mut s.items {
6785 if let SelectItem::Expr { expr, .. } = item {
6786 substitute_expr(expr, params)?;
6787 }
6788 }
6789 if let Some(w) = &mut s.where_ {
6790 substitute_expr(w, params)?;
6791 }
6792 if let Some(gs) = &mut s.group_by {
6793 for g in gs {
6794 substitute_expr(g, params)?;
6795 }
6796 }
6797 if let Some(h) = &mut s.having {
6798 substitute_expr(h, params)?;
6799 }
6800 for o in &mut s.order_by {
6801 substitute_expr(&mut o.expr, params)?;
6802 }
6803 for (_, peer) in &mut s.unions {
6804 substitute_select(peer, params)?;
6805 }
6806 Ok(())
6807}
6808
6809fn substitute_expr(e: &mut Expr, params: &[Value]) -> Result<(), EngineError> {
6810 if let Expr::Placeholder(n) = e {
6811 let idx = usize::from(*n).saturating_sub(1);
6812 let v = params.get(idx).ok_or_else(|| {
6813 EngineError::Eval(EvalError::PlaceholderOutOfRange {
6814 n: *n,
6815 bound: u16::try_from(params.len()).unwrap_or(u16::MAX),
6816 })
6817 })?;
6818 *e = Expr::Literal(value_to_literal(v.clone()));
6819 return Ok(());
6820 }
6821 match e {
6822 Expr::Binary { lhs, rhs, .. } => {
6823 substitute_expr(lhs, params)?;
6824 substitute_expr(rhs, params)?;
6825 }
6826 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
6827 substitute_expr(expr, params)?;
6828 }
6829 Expr::FunctionCall { args, .. } => {
6830 for a in args {
6831 substitute_expr(a, params)?;
6832 }
6833 }
6834 Expr::Like { expr, pattern, .. } => {
6835 substitute_expr(expr, params)?;
6836 substitute_expr(pattern, params)?;
6837 }
6838 Expr::Extract { source, .. } => substitute_expr(source, params)?,
6839 Expr::ScalarSubquery(s) => substitute_select(s, params)?,
6840 Expr::Exists { subquery, .. } => substitute_select(subquery, params)?,
6841 Expr::InSubquery { expr, subquery, .. } => {
6842 substitute_expr(expr, params)?;
6843 substitute_select(subquery, params)?;
6844 }
6845 Expr::WindowFunction {
6846 args,
6847 partition_by,
6848 order_by,
6849 ..
6850 } => {
6851 for a in args {
6852 substitute_expr(a, params)?;
6853 }
6854 for p in partition_by {
6855 substitute_expr(p, params)?;
6856 }
6857 for (e, _) in order_by {
6858 substitute_expr(e, params)?;
6859 }
6860 }
6861 Expr::Literal(_) | Expr::Column(_) => {}
6862 Expr::Placeholder(_) => unreachable!("Placeholder handled at top of fn"),
6864 }
6865 Ok(())
6866}
6867
6868fn sort_values_for_histogram(a: &Value, b: &Value) -> core::cmp::Ordering {
6886 use core::cmp::Ordering;
6887 match (a, b) {
6888 (Value::SmallInt(a), Value::SmallInt(b)) => a.cmp(b),
6889 (Value::Int(a), Value::Int(b)) => a.cmp(b),
6890 (Value::BigInt(a), Value::BigInt(b)) => a.cmp(b),
6891 (Value::SmallInt(a), Value::Int(b)) => i32::from(*a).cmp(b),
6892 (Value::Int(a), Value::SmallInt(b)) => a.cmp(&i32::from(*b)),
6893 (Value::Int(a), Value::BigInt(b)) => i64::from(*a).cmp(b),
6894 (Value::BigInt(a), Value::Int(b)) => a.cmp(&i64::from(*b)),
6895 (Value::SmallInt(a), Value::BigInt(b)) => i64::from(*a).cmp(b),
6896 (Value::BigInt(a), Value::SmallInt(b)) => a.cmp(&i64::from(*b)),
6897 (Value::Float(a), Value::Float(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
6898 (Value::Text(a), Value::Text(b)) | (Value::Json(a), Value::Json(b)) => a.cmp(b),
6899 (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
6900 (Value::Date(a), Value::Date(b)) => a.cmp(b),
6901 (Value::Timestamp(a), Value::Timestamp(b)) => a.cmp(b),
6902 (Value::SmallInt(n), Value::Float(x)) => {
6904 (f64::from(*n)).partial_cmp(x).unwrap_or(Ordering::Equal)
6905 }
6906 (Value::Float(x), Value::SmallInt(n)) => {
6907 x.partial_cmp(&f64::from(*n)).unwrap_or(Ordering::Equal)
6908 }
6909 (Value::Int(n), Value::Float(x)) => {
6910 (f64::from(*n)).partial_cmp(x).unwrap_or(Ordering::Equal)
6911 }
6912 (Value::Float(x), Value::Int(n)) => {
6913 x.partial_cmp(&f64::from(*n)).unwrap_or(Ordering::Equal)
6914 }
6915 (Value::BigInt(n), Value::Float(x)) => {
6916 #[allow(clippy::cast_precision_loss)]
6917 let nf = *n as f64;
6918 nf.partial_cmp(x).unwrap_or(Ordering::Equal)
6919 }
6920 (Value::Float(x), Value::BigInt(n)) => {
6921 #[allow(clippy::cast_precision_loss)]
6922 let nf = *n as f64;
6923 x.partial_cmp(&nf).unwrap_or(Ordering::Equal)
6924 }
6925 _ => canonical_value_repr(a).cmp(&canonical_value_repr(b)),
6928 }
6929}
6930
6931fn render_histogram_bounds(bounds: &[alloc::string::String]) -> alloc::string::String {
6938 let mut out = alloc::string::String::with_capacity(bounds.len() * 8 + 2);
6939 out.push('[');
6940 for (i, b) in bounds.iter().enumerate() {
6941 if i > 0 {
6942 out.push_str(", ");
6943 }
6944 let needs_quote = b.contains([',', '[', ']', '"']) || b.is_empty();
6945 if needs_quote {
6946 out.push('"');
6947 for ch in b.chars() {
6948 if ch == '"' || ch == '\\' {
6949 out.push('\\');
6950 }
6951 out.push(ch);
6952 }
6953 out.push('"');
6954 } else {
6955 out.push_str(b);
6956 }
6957 }
6958 out.push(']');
6959 out
6960}
6961
6962pub(crate) fn canonical_value_repr(v: &Value) -> alloc::string::String {
6972 match v {
6973 Value::Null => "NULL".to_string(),
6974 Value::SmallInt(n) => alloc::format!("{n}"),
6975 Value::Int(n) => alloc::format!("{n}"),
6976 Value::BigInt(n) => alloc::format!("{n}"),
6977 Value::Float(x) => alloc::format!("{x:?}"),
6978 Value::Text(s) | Value::Json(s) => s.clone(),
6979 Value::Bool(b) => if *b { "t" } else { "f" }.to_string(),
6980 Value::Date(d) => eval::format_date(*d),
6981 Value::Timestamp(t) => eval::format_timestamp(*t),
6982 Value::Interval { months, micros } => eval::format_interval(*months, *micros),
6983 Value::Numeric { scaled, scale } => eval::format_numeric(*scaled, *scale),
6984 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
6985 alloc::format!("{v:?}")
6989 }
6990 _ => alloc::format!("{v:?}"),
6994 }
6995}
6996
6997const fn is_internal_table_name(_name: &str) -> bool {
7004 false
7005}
7006
7007fn value_to_literal(v: Value) -> Literal {
7008 match v {
7009 Value::Null => Literal::Null,
7010 Value::SmallInt(n) => Literal::Integer(i64::from(n)),
7011 Value::Int(n) => Literal::Integer(i64::from(n)),
7012 Value::BigInt(n) => Literal::Integer(n),
7013 Value::Float(x) => Literal::Float(x),
7014 Value::Text(s) | Value::Json(s) => Literal::String(s),
7015 Value::Bool(b) => Literal::Bool(b),
7016 Value::Vector(v) => Literal::Vector(v),
7017 Value::Numeric { scaled, scale } => {
7018 Literal::String(eval::format_numeric(scaled, scale))
7019 }
7020 Value::Date(d) => Literal::String(eval::format_date(d)),
7021 Value::Timestamp(t) => Literal::String(eval::format_timestamp(t)),
7022 Value::Interval { months, micros } => Literal::Interval {
7023 months,
7024 micros,
7025 text: eval::format_interval(months, micros),
7026 },
7027 Value::Sq8Vector(q) => Literal::Vector(spg_storage::quantize::dequantize(&q)),
7030 Value::HalfVector(h) => Literal::Vector(h.to_f32_vec()),
7031 v => Literal::String(alloc::format!("{v:?}")),
7035 }
7036}
7037
7038fn rewrite_clock_calls(stmt: &mut Statement, now_micros: Option<i64>) {
7039 let Some(now) = now_micros else {
7040 return;
7041 };
7042 match stmt {
7043 Statement::Select(s) => rewrite_select_clock(s, now),
7044 Statement::Insert(ins) => {
7045 for row in &mut ins.rows {
7046 for e in row {
7047 rewrite_expr_clock(e, now);
7048 }
7049 }
7050 }
7051 _ => {}
7052 }
7053}
7054
7055fn rewrite_select_clock(s: &mut SelectStatement, now: i64) {
7056 for item in &mut s.items {
7057 if let SelectItem::Expr { expr, .. } = item {
7058 rewrite_expr_clock(expr, now);
7059 }
7060 }
7061 if let Some(w) = &mut s.where_ {
7062 rewrite_expr_clock(w, now);
7063 }
7064 if let Some(gs) = &mut s.group_by {
7065 for g in gs {
7066 rewrite_expr_clock(g, now);
7067 }
7068 }
7069 if let Some(h) = &mut s.having {
7070 rewrite_expr_clock(h, now);
7071 }
7072 for o in &mut s.order_by {
7073 rewrite_expr_clock(&mut o.expr, now);
7074 }
7075 for (_, peer) in &mut s.unions {
7076 rewrite_select_clock(peer, now);
7077 }
7078}
7079
7080fn rewrite_expr_clock(e: &mut Expr, now: i64) {
7088 if let Some(replacement) = clock_replacement_for(e, now) {
7092 *e = replacement;
7093 return;
7094 }
7095 match e {
7096 Expr::Binary { lhs, rhs, .. } => {
7097 rewrite_expr_clock(lhs, now);
7098 rewrite_expr_clock(rhs, now);
7099 }
7100 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
7101 rewrite_expr_clock(expr, now);
7102 }
7103 Expr::FunctionCall { args, .. } => {
7104 for a in args {
7105 rewrite_expr_clock(a, now);
7106 }
7107 }
7108 Expr::Like { expr, pattern, .. } => {
7109 rewrite_expr_clock(expr, now);
7110 rewrite_expr_clock(pattern, now);
7111 }
7112 Expr::Extract { source, .. } => rewrite_expr_clock(source, now),
7113 Expr::ScalarSubquery(s) => rewrite_select_clock(s, now),
7117 Expr::Exists { subquery, .. } => rewrite_select_clock(subquery, now),
7118 Expr::InSubquery { expr, subquery, .. } => {
7119 rewrite_expr_clock(expr, now);
7120 rewrite_select_clock(subquery, now);
7121 }
7122 Expr::WindowFunction {
7125 args,
7126 partition_by,
7127 order_by,
7128 ..
7129 } => {
7130 for a in args {
7131 rewrite_expr_clock(a, now);
7132 }
7133 for p in partition_by {
7134 rewrite_expr_clock(p, now);
7135 }
7136 for (e, _) in order_by {
7137 rewrite_expr_clock(e, now);
7138 }
7139 }
7140 Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {}
7141 }
7142}
7143
7144fn clock_replacement_for(e: &Expr, now: i64) -> Option<Expr> {
7151 let (kind, name) = match e {
7152 Expr::FunctionCall { name, args } if args.is_empty() => (ClockSite::Fn, name.as_str()),
7153 Expr::Column(c) if c.qualifier.is_none() => (ClockSite::BareIdent, c.name.as_str()),
7154 _ => return None,
7155 };
7156 let matched = match name.len() {
7159 3 if kind == ClockSite::Fn && name.eq_ignore_ascii_case("now") => Some(true),
7160 12 if name.eq_ignore_ascii_case("current_date") => Some(false),
7161 17 if name.eq_ignore_ascii_case("current_timestamp") => Some(true),
7162 _ => None,
7163 };
7164 let is_timestamp = matched?;
7165 let payload = if is_timestamp {
7166 now
7167 } else {
7168 now.div_euclid(86_400_000_000)
7169 };
7170 let target = if is_timestamp {
7171 spg_sql::ast::CastTarget::Timestamp
7172 } else {
7173 spg_sql::ast::CastTarget::Date
7174 };
7175 Some(Expr::Cast {
7176 expr: alloc::boxed::Box::new(Expr::Literal(spg_sql::ast::Literal::Integer(payload))),
7177 target,
7178 })
7179}
7180
7181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7182enum ClockSite {
7183 Fn,
7184 BareIdent,
7185}
7186
7187fn expand_group_by_all(s: &mut SelectStatement) {
7198 if !s.group_by_all {
7199 for (_, peer) in &mut s.unions {
7200 expand_group_by_all(peer);
7201 }
7202 return;
7203 }
7204 let mut groups: Vec<Expr> = Vec::new();
7205 for item in &s.items {
7206 if let SelectItem::Expr { expr, .. } = item
7207 && !aggregate::contains_aggregate(expr)
7208 {
7209 groups.push(expr.clone());
7210 }
7211 }
7212 s.group_by = Some(groups);
7213 s.group_by_all = false;
7214 for (_, peer) in &mut s.unions {
7215 expand_group_by_all(peer);
7216 }
7217}
7218
7219fn resolve_order_by_position(s: &mut SelectStatement) {
7220 for order in &mut s.order_by {
7225 match &order.expr {
7226 Expr::Literal(Literal::Integer(n)) if *n >= 1 => {
7227 if let Ok(idx_one_based) = usize::try_from(*n) {
7228 let idx = idx_one_based - 1;
7229 if idx < s.items.len()
7230 && let SelectItem::Expr { expr, .. } = &s.items[idx]
7231 {
7232 order.expr = expr.clone();
7233 }
7234 }
7235 }
7236 Expr::Column(c) if c.qualifier.is_none() => {
7237 for item in &s.items {
7239 if let SelectItem::Expr {
7240 expr,
7241 alias: Some(a),
7242 } = item
7243 && a == &c.name
7244 {
7245 order.expr = expr.clone();
7246 break;
7247 }
7248 }
7249 }
7250 _ => {}
7251 }
7252 }
7253 for (_, peer) in &mut s.unions {
7254 resolve_order_by_position(peer);
7255 }
7256}
7257
7258fn partial_sort_tagged(
7271 tagged: &mut Vec<(Vec<f64>, Row)>,
7272 keep: Option<usize>,
7273 descs: &[bool],
7274) {
7275 let cmp = |a: &(Vec<f64>, Row), b: &(Vec<f64>, Row)| cmp_multi_key(&a.0, &b.0, descs);
7276 match keep {
7277 Some(k) if k < tagged.len() && k > 0 => {
7278 let pivot = k - 1;
7279 tagged.select_nth_unstable_by(pivot, cmp);
7280 tagged[..k].sort_by(cmp);
7281 tagged.truncate(k);
7282 }
7283 _ => {
7284 tagged.sort_by(cmp);
7285 }
7286 }
7287}
7288
7289fn sort_by_keys(tagged: &mut [(Vec<f64>, Row)], descs: &[bool]) {
7290 tagged.sort_by(|a, b| cmp_multi_key(&a.0, &b.0, descs));
7291}
7292
7293fn cmp_multi_key(a: &[f64], b: &[f64], descs: &[bool]) -> core::cmp::Ordering {
7297 use core::cmp::Ordering;
7298 for (i, (ka, kb)) in a.iter().zip(b.iter()).enumerate() {
7299 let ord = ka.partial_cmp(kb).unwrap_or(Ordering::Equal);
7300 let ord = if descs.get(i).copied().unwrap_or(false) {
7301 ord.reverse()
7302 } else {
7303 ord
7304 };
7305 if ord != Ordering::Equal {
7306 return ord;
7307 }
7308 }
7309 Ordering::Equal
7310}
7311
7312fn build_order_keys(
7315 order_by: &[OrderBy],
7316 row: &Row,
7317 ctx: &EvalContext,
7318) -> Result<Vec<f64>, EngineError> {
7319 let mut keys = Vec::with_capacity(order_by.len());
7320 for o in order_by {
7321 let v = eval::eval_expr(&o.expr, row, ctx)?;
7322 keys.push(value_to_order_key(&v)?);
7323 }
7324 Ok(keys)
7325}
7326
7327fn apply_offset_and_limit(rows: &mut Vec<Row>, offset: Option<u32>, limit: Option<u32>) {
7331 if let Some(off) = offset {
7332 let off = off as usize;
7333 if off >= rows.len() {
7334 rows.clear();
7335 } else {
7336 rows.drain(..off);
7337 }
7338 }
7339 if let Some(n) = limit {
7340 rows.truncate(n as usize);
7341 }
7342}
7343
7344fn resolve_foreign_key(
7358 local_table_name: &str,
7359 local_cols: &[ColumnSchema],
7360 fk: spg_sql::ast::ForeignKeyConstraint,
7361 catalog: &Catalog,
7362) -> Result<spg_storage::ForeignKeyConstraint, EngineError> {
7363 let mut local_columns = Vec::with_capacity(fk.columns.len());
7365 for name in &fk.columns {
7366 let pos = local_cols
7367 .iter()
7368 .position(|c| c.name == *name)
7369 .ok_or_else(|| {
7370 EngineError::Unsupported(alloc::format!(
7371 "FOREIGN KEY references unknown local column {name:?}"
7372 ))
7373 })?;
7374 local_columns.push(pos);
7375 }
7376 let is_self_ref = fk.parent_table == local_table_name;
7380 let (parent_cols_for_lookup, parent_table_str): (&[ColumnSchema], &str) = if is_self_ref {
7381 (local_cols, local_table_name)
7382 } else {
7383 let parent_table = catalog.get(&fk.parent_table).ok_or_else(|| {
7384 EngineError::Storage(StorageError::TableNotFound {
7385 name: fk.parent_table.clone(),
7386 })
7387 })?;
7388 (parent_table.schema().columns.as_slice(), fk.parent_table.as_str())
7389 };
7390 let parent_columns: Vec<usize> = if fk.parent_columns.is_empty() {
7395 if fk.columns.len() != 1 {
7396 return Err(EngineError::Unsupported(
7397 "composite FOREIGN KEY without explicit parent column list is not supported \
7398 — list the parent columns explicitly"
7399 .into(),
7400 ));
7401 }
7402 let pos = pick_pk_index_column(catalog, parent_table_str, is_self_ref, local_cols)
7404 .ok_or_else(|| {
7405 EngineError::Unsupported(alloc::format!(
7406 "parent table {parent_table_str:?} has no PRIMARY-key / UNIQUE BTree index \
7407 to default the FOREIGN KEY against"
7408 ))
7409 })?;
7410 alloc::vec![pos]
7411 } else {
7412 let mut out = Vec::with_capacity(fk.parent_columns.len());
7413 for name in &fk.parent_columns {
7414 let pos = parent_cols_for_lookup
7415 .iter()
7416 .position(|c| c.name == *name)
7417 .ok_or_else(|| {
7418 EngineError::Unsupported(alloc::format!(
7419 "FOREIGN KEY references unknown parent column \
7420 {name:?} on table {parent_table_str:?}"
7421 ))
7422 })?;
7423 out.push(pos);
7424 }
7425 out
7426 };
7427 if parent_columns.len() != local_columns.len() {
7428 return Err(EngineError::Unsupported(alloc::format!(
7429 "FOREIGN KEY arity mismatch: {} local columns vs {} parent columns",
7430 local_columns.len(),
7431 parent_columns.len()
7432 )));
7433 }
7434 if !is_self_ref {
7444 let parent_table = catalog
7445 .get(&fk.parent_table)
7446 .expect("checked above");
7447 let primary_parent_col = parent_columns[0];
7448 let has_btree = parent_table.schema().columns.get(primary_parent_col).is_some()
7449 && parent_table
7450 .indices()
7451 .iter()
7452 .any(|idx| {
7453 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7454 && idx.column_position == primary_parent_col
7455 && idx.partial_predicate.is_none()
7456 });
7457 if !has_btree {
7458 return Err(EngineError::Unsupported(alloc::format!(
7459 "FOREIGN KEY parent column on {:?} is not covered by an unconditional BTree \
7460 index — create one with `CREATE INDEX ... ON {} ({})` first",
7461 parent_table_str,
7462 parent_table_str,
7463 parent_table.schema().columns[primary_parent_col].name,
7464 )));
7465 }
7466 }
7467 let on_delete = fk_action_sql_to_storage(fk.on_delete);
7468 let on_update = fk_action_sql_to_storage(fk.on_update);
7469 Ok(spg_storage::ForeignKeyConstraint {
7470 name: fk.name,
7471 local_columns,
7472 parent_table: fk.parent_table,
7473 parent_columns,
7474 on_delete,
7475 on_update,
7476 })
7477}
7478
7479fn pick_pk_index_column(
7485 catalog: &Catalog,
7486 parent_name: &str,
7487 is_self_ref: bool,
7488 local_cols: &[ColumnSchema],
7489) -> Option<usize> {
7490 if is_self_ref {
7491 let _ = local_cols;
7495 return Some(0);
7496 }
7497 let parent = catalog.get(parent_name)?;
7498 parent.indices().iter().find_map(|idx| {
7499 if matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7500 && idx.partial_predicate.is_none()
7501 && idx.included_columns.is_empty()
7502 && idx.expression.is_none()
7503 {
7504 Some(idx.column_position)
7505 } else {
7506 None
7507 }
7508 })
7509}
7510
7511fn resolve_on_conflict_columns(
7518 catalog: &Catalog,
7519 table_name: &str,
7520 target: &[String],
7521) -> Result<Vec<usize>, EngineError> {
7522 let table = catalog.get(table_name).ok_or_else(|| {
7523 EngineError::Storage(StorageError::TableNotFound {
7524 name: table_name.into(),
7525 })
7526 })?;
7527 if target.is_empty() {
7528 let pos = table
7529 .indices()
7530 .iter()
7531 .find_map(|idx| {
7532 if matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7533 && idx.partial_predicate.is_none()
7534 && idx.included_columns.is_empty()
7535 && idx.expression.is_none()
7536 {
7537 Some(idx.column_position)
7538 } else {
7539 None
7540 }
7541 })
7542 .ok_or_else(|| {
7543 EngineError::Unsupported(alloc::format!(
7544 "ON CONFLICT without target requires a UNIQUE BTree index on {table_name:?}"
7545 ))
7546 })?;
7547 return Ok(alloc::vec![pos]);
7548 }
7549 let mut out = Vec::with_capacity(target.len());
7550 for name in target {
7551 let pos = table
7552 .schema()
7553 .columns
7554 .iter()
7555 .position(|c| c.name == *name)
7556 .ok_or_else(|| {
7557 EngineError::Unsupported(alloc::format!(
7558 "ON CONFLICT target column {name:?} not found on {table_name:?}"
7559 ))
7560 })?;
7561 out.push(pos);
7562 }
7563 Ok(out)
7564}
7565
7566fn on_conflict_key_exists(
7569 catalog: &Catalog,
7570 table_name: &str,
7571 column_pos: usize,
7572 key: &Value,
7573) -> bool {
7574 let Some(table) = catalog.get(table_name) else {
7575 return false;
7576 };
7577 let Some(idx_key) = spg_storage::IndexKey::from_value(key) else {
7578 return false;
7579 };
7580 table.indices().iter().any(|idx| {
7581 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7582 && idx.column_position == column_pos
7583 && idx.partial_predicate.is_none()
7584 && !idx.lookup_eq(&idx_key).is_empty()
7585 })
7586}
7587
7588fn lookup_row_position_by_keys(
7594 catalog: &Catalog,
7595 table_name: &str,
7596 column_positions: &[usize],
7597 key: &[&Value],
7598) -> Option<usize> {
7599 let table = catalog.get(table_name)?;
7600 table.rows().iter().position(|r| {
7601 column_positions
7602 .iter()
7603 .enumerate()
7604 .all(|(i, &pos)| r.values.get(pos) == Some(key[i]))
7605 })
7606}
7607
7608fn on_conflict_keys_exist(
7613 catalog: &Catalog,
7614 table_name: &str,
7615 column_positions: &[usize],
7616 key: &[&Value],
7617) -> bool {
7618 if column_positions.len() == 1 {
7619 return on_conflict_key_exists(
7620 catalog,
7621 table_name,
7622 column_positions[0],
7623 key[0],
7624 );
7625 }
7626 let Some(table) = catalog.get(table_name) else {
7627 return false;
7628 };
7629 table.rows().iter().any(|r| {
7630 column_positions
7631 .iter()
7632 .enumerate()
7633 .all(|(i, &pos)| r.values.get(pos) == Some(key[i]))
7634 })
7635}
7636
7637fn apply_on_conflict_assignments(
7650 catalog: &Catalog,
7651 table_name: &str,
7652 target_pos: usize,
7653 incoming: &[Value],
7654 assignments: &[(String, Expr)],
7655 where_: Option<&Expr>,
7656) -> Result<Option<Vec<Value>>, EngineError> {
7657 let table = catalog.get(table_name).ok_or_else(|| {
7658 EngineError::Storage(StorageError::TableNotFound {
7659 name: table_name.into(),
7660 })
7661 })?;
7662 let schema_cols = table.schema().columns.clone();
7663 let existing = table
7664 .rows()
7665 .get(target_pos)
7666 .ok_or_else(|| {
7667 EngineError::Unsupported(alloc::format!(
7668 "ON CONFLICT DO UPDATE: row position {target_pos} out of bounds on {table_name:?}"
7669 ))
7670 })?
7671 .clone();
7672 let ctx = eval::EvalContext::new(&schema_cols, Some(table_name));
7673 if let Some(w) = where_ {
7675 let pred = w.clone();
7676 let pred = substitute_excluded_refs(pred, &schema_cols, incoming);
7677 let v = eval::eval_expr(&pred, &existing, &ctx)?;
7678 if !matches!(v, Value::Bool(true)) {
7679 return Ok(None);
7680 }
7681 }
7682 let mut new_values = existing.values.clone();
7683 for (col_name, expr) in assignments {
7684 let target_idx = schema_cols
7685 .iter()
7686 .position(|c| c.name == *col_name)
7687 .ok_or_else(|| {
7688 EngineError::Eval(EvalError::ColumnNotFound {
7689 name: col_name.clone(),
7690 })
7691 })?;
7692 let sub = substitute_excluded_refs(expr.clone(), &schema_cols, incoming);
7693 let v = eval::eval_expr(&sub, &existing, &ctx)?;
7694 new_values[target_idx] =
7695 coerce_value(v, schema_cols[target_idx].ty, col_name, target_idx)?;
7696 }
7697 Ok(Some(new_values))
7698}
7699
7700fn substitute_excluded_refs(
7705 expr: Expr,
7706 schema_cols: &[ColumnSchema],
7707 incoming: &[Value],
7708) -> Expr {
7709 use spg_sql::ast::ColumnName;
7710 match expr {
7711 Expr::Column(ColumnName { qualifier, name })
7712 if qualifier
7713 .as_deref()
7714 .is_some_and(|q| q.eq_ignore_ascii_case("excluded")) =>
7715 {
7716 let pos = schema_cols.iter().position(|c| c.name == name);
7717 match pos {
7718 Some(p) => {
7719 let v = incoming.get(p).cloned().unwrap_or(Value::Null);
7720 value_to_literal_expr(v).unwrap_or_else(|_| {
7721 Expr::Literal(spg_sql::ast::Literal::Null)
7722 })
7723 }
7724 None => Expr::Column(ColumnName { qualifier, name }),
7725 }
7726 }
7727 Expr::Binary { op, lhs, rhs } => Expr::Binary {
7728 op,
7729 lhs: Box::new(substitute_excluded_refs(*lhs, schema_cols, incoming)),
7730 rhs: Box::new(substitute_excluded_refs(*rhs, schema_cols, incoming)),
7731 },
7732 Expr::Unary { op, expr } => Expr::Unary {
7733 op,
7734 expr: Box::new(substitute_excluded_refs(*expr, schema_cols, incoming)),
7735 },
7736 Expr::FunctionCall { name, args } => Expr::FunctionCall {
7737 name,
7738 args: args
7739 .into_iter()
7740 .map(|a| substitute_excluded_refs(a, schema_cols, incoming))
7741 .collect(),
7742 },
7743 other => other,
7744 }
7745}
7746
7747fn enforce_fk_inserts(
7764 catalog: &Catalog,
7765 child_table: &str,
7766 fks: &[spg_storage::ForeignKeyConstraint],
7767 rows: &[Vec<Value>],
7768) -> Result<(), EngineError> {
7769 for fk in fks {
7770 let parent_is_self = fk.parent_table == child_table;
7771 let parent = if parent_is_self {
7772 catalog.get(child_table).ok_or_else(|| {
7775 EngineError::Storage(StorageError::TableNotFound {
7776 name: child_table.into(),
7777 })
7778 })?
7779 } else {
7780 catalog.get(&fk.parent_table).ok_or_else(|| {
7781 EngineError::Storage(StorageError::TableNotFound {
7782 name: fk.parent_table.clone(),
7783 })
7784 })?
7785 };
7786 for (batch_idx, row_values) in rows.iter().enumerate() {
7787 if fk.local_columns.len() == 1 {
7791 let v = &row_values[fk.local_columns[0]];
7792 if matches!(v, Value::Null) {
7793 continue;
7794 }
7795 let parent_col = fk.parent_columns[0];
7796 let key = spg_storage::IndexKey::from_value(v).ok_or_else(|| {
7797 EngineError::Unsupported(alloc::format!(
7798 "FOREIGN KEY column value of type {:?} is not index-eligible",
7799 v.data_type()
7800 ))
7801 })?;
7802 let present_committed = parent.indices().iter().any(|idx| {
7803 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
7804 && idx.column_position == parent_col
7805 && idx.partial_predicate.is_none()
7806 && !idx.lookup_eq(&key).is_empty()
7807 });
7808 let present_in_batch = parent_is_self
7812 && rows[..batch_idx].iter().any(|earlier| {
7813 earlier.get(parent_col) == Some(v)
7814 });
7815 if !(present_committed || present_in_batch) {
7816 return Err(EngineError::Unsupported(alloc::format!(
7817 "FOREIGN KEY violation: no parent row in {:?} where {} = {:?}",
7818 fk.parent_table,
7819 parent
7820 .schema()
7821 .columns
7822 .get(parent_col)
7823 .map_or("?", |c| c.name.as_str()),
7824 v,
7825 )));
7826 }
7827 } else {
7828 if fk.local_columns
7832 .iter()
7833 .all(|&i| matches!(row_values.get(i), Some(Value::Null)))
7834 {
7835 continue;
7836 }
7837 let local: Vec<&Value> = fk.local_columns.iter().map(|&i| &row_values[i]).collect();
7838 let parent_match_committed = parent.rows().iter().any(|prow| {
7839 fk.parent_columns
7840 .iter()
7841 .enumerate()
7842 .all(|(i, &pi)| prow.values.get(pi) == Some(local[i]))
7843 });
7844 let parent_match_in_batch = parent_is_self
7845 && rows[..batch_idx].iter().any(|earlier| {
7846 fk.parent_columns
7847 .iter()
7848 .enumerate()
7849 .all(|(i, &pi)| earlier.get(pi) == Some(local[i]))
7850 });
7851 if !(parent_match_committed || parent_match_in_batch) {
7852 return Err(EngineError::Unsupported(alloc::format!(
7853 "FOREIGN KEY violation: no parent row in {:?} matching composite key",
7854 fk.parent_table,
7855 )));
7856 }
7857 }
7858 }
7859 }
7860 Ok(())
7861}
7862
7863#[derive(Debug, Clone)]
7867struct FkChildStep {
7868 child_table: String,
7869 action: FkChildAction,
7870}
7871
7872#[derive(Debug, Clone)]
7873enum FkChildAction {
7874 Delete { positions: Vec<usize> },
7876 SetNull {
7880 positions: Vec<usize>,
7881 columns: Vec<usize>,
7882 },
7883 SetDefault {
7887 positions: Vec<usize>,
7888 columns: Vec<usize>,
7889 defaults: Vec<Value>,
7890 },
7891}
7892
7893fn plan_fk_parent_deletions(
7909 catalog: &Catalog,
7910 parent_table_name: &str,
7911 to_delete_positions: &[usize],
7912 to_delete_rows: &[Vec<Value>],
7913) -> Result<Vec<FkChildStep>, EngineError> {
7914 use alloc::collections::{BTreeMap, BTreeSet};
7915 if to_delete_rows.is_empty() {
7916 return Ok(Vec::new());
7917 }
7918 let mut delete_plan: BTreeMap<String, BTreeSet<usize>> = BTreeMap::new();
7919 let mut setnull_plan: BTreeMap<String, BTreeSet<(usize, usize)>> = BTreeMap::new();
7921 let mut setdefault_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> =
7922 BTreeMap::new();
7923 let mut visited: BTreeSet<(String, usize)> = BTreeSet::new();
7924 for &p in to_delete_positions {
7925 visited.insert((parent_table_name.to_string(), p));
7926 }
7927 let mut work: Vec<(String, Vec<Value>)> = to_delete_rows
7928 .iter()
7929 .map(|r| (parent_table_name.to_string(), r.clone()))
7930 .collect();
7931 while let Some((cur_parent, parent_row)) = work.pop() {
7932 for child_name in catalog.table_names() {
7933 let child = catalog
7934 .get(&child_name)
7935 .expect("table_names → catalog.get round-trip is total");
7936 for fk in &child.schema().foreign_keys {
7937 if fk.parent_table != cur_parent {
7938 continue;
7939 }
7940 let parent_key: Vec<&Value> = fk
7941 .parent_columns
7942 .iter()
7943 .map(|&pi| &parent_row[pi])
7944 .collect();
7945 if parent_key.iter().any(|v| matches!(v, Value::Null)) {
7946 continue;
7947 }
7948 for (child_row_idx, child_row) in child.rows().iter().enumerate() {
7949 if child_name == cur_parent
7950 && visited.contains(&(child_name.clone(), child_row_idx))
7951 {
7952 continue;
7953 }
7954 let matches_key = fk
7955 .local_columns
7956 .iter()
7957 .enumerate()
7958 .all(|(i, &li)| child_row.values.get(li) == Some(parent_key[i]));
7959 if !matches_key {
7960 continue;
7961 }
7962 match fk.on_delete {
7963 spg_storage::FkAction::Restrict
7964 | spg_storage::FkAction::NoAction => {
7965 return Err(EngineError::Unsupported(alloc::format!(
7966 "FOREIGN KEY violation: DELETE on {cur_parent:?} is \
7967 restricted by FK from {child_name:?}.{:?}",
7968 fk.local_columns,
7969 )));
7970 }
7971 spg_storage::FkAction::Cascade => {
7972 if visited.insert((child_name.clone(), child_row_idx)) {
7973 delete_plan
7974 .entry(child_name.clone())
7975 .or_default()
7976 .insert(child_row_idx);
7977 work.push((child_name.clone(), child_row.values.clone()));
7978 }
7979 }
7980 spg_storage::FkAction::SetNull => {
7981 for &li in &fk.local_columns {
7983 let col = child.schema().columns.get(li).ok_or_else(|| {
7984 EngineError::Unsupported(alloc::format!(
7985 "FK local column {li} missing in {child_name:?}"
7986 ))
7987 })?;
7988 if !col.nullable {
7989 return Err(EngineError::Unsupported(alloc::format!(
7990 "FOREIGN KEY ON DELETE SET NULL: column \
7991 {child_name:?}.{:?} is NOT NULL — cannot SET NULL",
7992 col.name,
7993 )));
7994 }
7995 }
7996 let entry = setnull_plan.entry(child_name.clone()).or_default();
7997 for &li in &fk.local_columns {
7998 entry.insert((child_row_idx, li));
7999 }
8000 }
8001 spg_storage::FkAction::SetDefault => {
8002 let entry =
8004 setdefault_plan.entry(child_name.clone()).or_default();
8005 for &li in &fk.local_columns {
8006 let col = child.schema().columns.get(li).ok_or_else(|| {
8007 EngineError::Unsupported(alloc::format!(
8008 "FK local column {li} missing in {child_name:?}"
8009 ))
8010 })?;
8011 let default = col.default.clone().ok_or_else(|| {
8012 EngineError::Unsupported(alloc::format!(
8013 "FOREIGN KEY ON DELETE SET DEFAULT: column \
8014 {child_name:?}.{:?} has no DEFAULT declared",
8015 col.name,
8016 ))
8017 })?;
8018 entry.insert((child_row_idx, li), default);
8019 }
8020 }
8021 }
8022 }
8023 }
8024 }
8025 }
8026 let mut steps: Vec<FkChildStep> = Vec::new();
8034 for (child_table, entries) in setnull_plan {
8035 let (positions, columns): (Vec<usize>, Vec<usize>) = entries.into_iter().unzip();
8036 steps.push(FkChildStep {
8037 child_table,
8038 action: FkChildAction::SetNull { positions, columns },
8039 });
8040 }
8041 for (child_table, entries) in setdefault_plan {
8042 let mut positions = Vec::with_capacity(entries.len());
8043 let mut columns = Vec::with_capacity(entries.len());
8044 let mut defaults = Vec::with_capacity(entries.len());
8045 for ((p, c), v) in entries {
8046 positions.push(p);
8047 columns.push(c);
8048 defaults.push(v);
8049 }
8050 steps.push(FkChildStep {
8051 child_table,
8052 action: FkChildAction::SetDefault {
8053 positions,
8054 columns,
8055 defaults,
8056 },
8057 });
8058 }
8059 for (child_table, positions) in delete_plan {
8060 steps.push(FkChildStep {
8061 child_table,
8062 action: FkChildAction::Delete {
8063 positions: positions.into_iter().collect(),
8064 },
8065 });
8066 }
8067 Ok(steps)
8068}
8069
8070fn plan_fk_parent_updates(
8087 catalog: &Catalog,
8088 parent_table_name: &str,
8089 plan_with_old: &[(usize, Vec<Value>, Vec<Value>)],
8090) -> Result<Vec<FkChildStep>, EngineError> {
8091 use alloc::collections::BTreeMap;
8092 if plan_with_old.is_empty() {
8093 return Ok(Vec::new());
8094 }
8095 let delete_plan: BTreeMap<String, alloc::collections::BTreeSet<usize>> = BTreeMap::new();
8100 let mut setnull_plan: BTreeMap<
8101 String,
8102 alloc::collections::BTreeSet<(usize, usize)>,
8103 > = BTreeMap::new();
8104 let mut setdefault_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> =
8105 BTreeMap::new();
8106 let mut cascade_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> = BTreeMap::new();
8108
8109 for child_name in catalog.table_names() {
8110 let child = catalog
8111 .get(&child_name)
8112 .expect("table_names → catalog.get total");
8113 for fk in &child.schema().foreign_keys {
8114 if fk.parent_table != parent_table_name {
8115 continue;
8116 }
8117 for (_pos, old_row, new_row) in plan_with_old {
8118 let key_changed = fk
8120 .parent_columns
8121 .iter()
8122 .any(|&pi| old_row.get(pi) != new_row.get(pi));
8123 if !key_changed {
8124 continue;
8125 }
8126 let old_key: Vec<&Value> = fk
8128 .parent_columns
8129 .iter()
8130 .map(|&pi| &old_row[pi])
8131 .collect();
8132 if old_key.iter().any(|v| matches!(v, Value::Null)) {
8133 continue;
8135 }
8136 let new_key: Vec<&Value> = fk
8137 .parent_columns
8138 .iter()
8139 .map(|&pi| &new_row[pi])
8140 .collect();
8141 for (child_row_idx, child_row) in child.rows().iter().enumerate() {
8142 if child_name == parent_table_name
8145 && plan_with_old
8146 .iter()
8147 .any(|(p, _, _)| *p == child_row_idx)
8148 {
8149 continue;
8150 }
8151 let matches_key = fk
8152 .local_columns
8153 .iter()
8154 .enumerate()
8155 .all(|(i, &li)| child_row.values.get(li) == Some(old_key[i]));
8156 if !matches_key {
8157 continue;
8158 }
8159 match fk.on_update {
8160 spg_storage::FkAction::Restrict
8161 | spg_storage::FkAction::NoAction => {
8162 return Err(EngineError::Unsupported(alloc::format!(
8163 "FOREIGN KEY violation: UPDATE on {parent_table_name:?} PK is \
8164 restricted by FK from {child_name:?}.{:?}",
8165 fk.local_columns,
8166 )));
8167 }
8168 spg_storage::FkAction::Cascade => {
8169 let entry = cascade_plan.entry(child_name.clone()).or_default();
8171 for (i, &li) in fk.local_columns.iter().enumerate() {
8172 entry.insert((child_row_idx, li), new_key[i].clone());
8173 }
8174 }
8175 spg_storage::FkAction::SetNull => {
8176 for &li in &fk.local_columns {
8177 let col = child.schema().columns.get(li).ok_or_else(|| {
8178 EngineError::Unsupported(alloc::format!(
8179 "FK local column {li} missing in {child_name:?}"
8180 ))
8181 })?;
8182 if !col.nullable {
8183 return Err(EngineError::Unsupported(alloc::format!(
8184 "FOREIGN KEY ON UPDATE SET NULL: column \
8185 {child_name:?}.{:?} is NOT NULL",
8186 col.name,
8187 )));
8188 }
8189 }
8190 let entry = setnull_plan.entry(child_name.clone()).or_default();
8191 for &li in &fk.local_columns {
8192 entry.insert((child_row_idx, li));
8193 }
8194 }
8195 spg_storage::FkAction::SetDefault => {
8196 let entry =
8197 setdefault_plan.entry(child_name.clone()).or_default();
8198 for &li in &fk.local_columns {
8199 let col = child.schema().columns.get(li).ok_or_else(|| {
8200 EngineError::Unsupported(alloc::format!(
8201 "FK local column {li} missing in {child_name:?}"
8202 ))
8203 })?;
8204 let default = col.default.clone().ok_or_else(|| {
8205 EngineError::Unsupported(alloc::format!(
8206 "FOREIGN KEY ON UPDATE SET DEFAULT: column \
8207 {child_name:?}.{:?} has no DEFAULT",
8208 col.name,
8209 ))
8210 })?;
8211 entry.insert((child_row_idx, li), default);
8212 }
8213 }
8214 }
8215 }
8216 }
8217 }
8218 }
8219 let mut steps: Vec<FkChildStep> = Vec::new();
8222 for (child_table, entries) in cascade_plan {
8223 let mut positions = Vec::with_capacity(entries.len());
8224 let mut columns = Vec::with_capacity(entries.len());
8225 let mut defaults = Vec::with_capacity(entries.len());
8226 for ((p, c), v) in entries {
8227 positions.push(p);
8228 columns.push(c);
8229 defaults.push(v);
8230 }
8231 steps.push(FkChildStep {
8236 child_table,
8237 action: FkChildAction::SetDefault {
8238 positions,
8239 columns,
8240 defaults,
8241 },
8242 });
8243 }
8244 for (child_table, entries) in setnull_plan {
8245 let (positions, columns): (Vec<usize>, Vec<usize>) = entries.into_iter().unzip();
8246 steps.push(FkChildStep {
8247 child_table,
8248 action: FkChildAction::SetNull { positions, columns },
8249 });
8250 }
8251 for (child_table, entries) in setdefault_plan {
8252 let mut positions = Vec::with_capacity(entries.len());
8253 let mut columns = Vec::with_capacity(entries.len());
8254 let mut defaults = Vec::with_capacity(entries.len());
8255 for ((p, c), v) in entries {
8256 positions.push(p);
8257 columns.push(c);
8258 defaults.push(v);
8259 }
8260 steps.push(FkChildStep {
8261 child_table,
8262 action: FkChildAction::SetDefault {
8263 positions,
8264 columns,
8265 defaults,
8266 },
8267 });
8268 }
8269 let _ = delete_plan; Ok(steps)
8271}
8272
8273fn apply_fk_child_step(
8277 catalog: &mut Catalog,
8278 step: &FkChildStep,
8279) -> Result<(), EngineError> {
8280 let child = catalog.get_mut(&step.child_table).ok_or_else(|| {
8281 EngineError::Storage(StorageError::TableNotFound {
8282 name: step.child_table.clone(),
8283 })
8284 })?;
8285 match &step.action {
8286 FkChildAction::Delete { positions } => {
8287 let _ = child.delete_rows(positions);
8288 }
8289 FkChildAction::SetNull { positions, columns } => {
8290 apply_per_cell_writes(child, positions, columns, |_| Value::Null)?;
8291 }
8292 FkChildAction::SetDefault {
8293 positions,
8294 columns,
8295 defaults,
8296 } => {
8297 apply_per_cell_writes(child, positions, columns, |i| defaults[i].clone())?;
8298 }
8299 }
8300 Ok(())
8301}
8302
8303fn apply_per_cell_writes(
8309 child: &mut spg_storage::Table,
8310 positions: &[usize],
8311 columns: &[usize],
8312 mut value_for: impl FnMut(usize) -> Value,
8313) -> Result<(), EngineError> {
8314 use alloc::collections::BTreeMap;
8315 let mut by_row: BTreeMap<usize, Vec<(usize, Value)>> = BTreeMap::new();
8316 for i in 0..positions.len() {
8317 by_row
8318 .entry(positions[i])
8319 .or_default()
8320 .push((columns[i], value_for(i)));
8321 }
8322 for (pos, mutations) in by_row {
8323 let mut new_values = child.rows()[pos].values.clone();
8324 for (col, v) in mutations {
8325 if let Some(slot) = new_values.get_mut(col) {
8326 *slot = v;
8327 }
8328 }
8329 child
8330 .update_row(pos, new_values)
8331 .map_err(EngineError::Storage)?;
8332 }
8333 Ok(())
8334}
8335
8336fn fk_action_sql_to_storage(a: spg_sql::ast::FkAction) -> spg_storage::FkAction {
8337 match a {
8338 spg_sql::ast::FkAction::Restrict => spg_storage::FkAction::Restrict,
8339 spg_sql::ast::FkAction::Cascade => spg_storage::FkAction::Cascade,
8340 spg_sql::ast::FkAction::SetNull => spg_storage::FkAction::SetNull,
8341 spg_sql::ast::FkAction::SetDefault => spg_storage::FkAction::SetDefault,
8342 spg_sql::ast::FkAction::NoAction => spg_storage::FkAction::NoAction,
8343 }
8344}
8345
8346fn column_def_to_schema(c: ColumnDef) -> Result<ColumnSchema, EngineError> {
8347 let ty = column_type_to_data_type(c.ty);
8348 let mut schema = ColumnSchema::new(c.name.clone(), ty, c.nullable);
8349 if let Some(default_expr) = c.default {
8350 let raw = literal_expr_to_value(default_expr)?;
8354 let coerced = coerce_value(raw, ty, &c.name, 0)?;
8355 schema = schema.with_default(coerced);
8356 }
8357 if c.auto_increment {
8358 if !matches!(ty, DataType::SmallInt | DataType::Int | DataType::BigInt) {
8360 return Err(EngineError::Unsupported(alloc::format!(
8361 "AUTO_INCREMENT requires an integer column type, got {ty:?}"
8362 )));
8363 }
8364 schema = schema.with_auto_increment();
8365 }
8366 Ok(schema)
8367}
8368
8369const fn column_type_to_data_type(t: ColumnTypeName) -> DataType {
8370 match t {
8371 ColumnTypeName::SmallInt => DataType::SmallInt,
8372 ColumnTypeName::Int => DataType::Int,
8373 ColumnTypeName::BigInt => DataType::BigInt,
8374 ColumnTypeName::Float => DataType::Float,
8375 ColumnTypeName::Text => DataType::Text,
8376 ColumnTypeName::Varchar(n) => DataType::Varchar(n),
8377 ColumnTypeName::Char(n) => DataType::Char(n),
8378 ColumnTypeName::Bool => DataType::Bool,
8379 ColumnTypeName::Vector { dim, encoding } => DataType::Vector {
8380 dim,
8381 encoding: match encoding {
8382 SqlVecEncoding::F32 => VecEncoding::F32,
8383 SqlVecEncoding::Sq8 => VecEncoding::Sq8,
8384 SqlVecEncoding::F16 => VecEncoding::F16,
8385 },
8386 },
8387 ColumnTypeName::Numeric(precision, scale) => DataType::Numeric { precision, scale },
8388 ColumnTypeName::Date => DataType::Date,
8389 ColumnTypeName::Timestamp => DataType::Timestamp,
8390 ColumnTypeName::Timestamptz => DataType::Timestamptz,
8391 ColumnTypeName::Json => DataType::Json,
8392 ColumnTypeName::Jsonb => DataType::Jsonb,
8393 }
8394}
8395
8396fn literal_expr_to_value(expr: Expr) -> Result<Value, EngineError> {
8400 match expr {
8401 Expr::Literal(l) => Ok(literal_to_value(l)),
8402 Expr::Cast { expr, target } => {
8403 let inner_value = literal_expr_to_value(*expr)?;
8404 crate::eval::cast_value(inner_value, target).map_err(EngineError::Eval)
8405 }
8406 Expr::Unary {
8407 op: UnOp::Neg,
8408 expr,
8409 } => match *expr {
8410 Expr::Literal(Literal::Integer(n)) => {
8411 let neg = n.checked_neg().ok_or_else(|| {
8414 EngineError::Unsupported("integer literal overflow on negation".into())
8415 })?;
8416 Ok(int_value_for(neg))
8417 }
8418 Expr::Literal(Literal::Float(x)) => Ok(Value::Float(-x)),
8419 other => Err(EngineError::Unsupported(alloc::format!(
8420 "unary minus over non-literal expression: {other:?}"
8421 ))),
8422 },
8423 other => Err(EngineError::Unsupported(alloc::format!(
8424 "non-literal INSERT value expression: {other:?}"
8425 ))),
8426 }
8427}
8428
8429fn literal_to_value(l: Literal) -> Value {
8430 match l {
8431 Literal::Integer(n) => int_value_for(n),
8432 Literal::Float(x) => Value::Float(x),
8433 Literal::String(s) => Value::Text(s),
8434 Literal::Bool(b) => Value::Bool(b),
8435 Literal::Null => Value::Null,
8436 Literal::Vector(v) => Value::Vector(v),
8437 Literal::Interval { months, micros, .. } => Value::Interval { months, micros },
8438 }
8439}
8440
8441fn int_value_for(n: i64) -> Value {
8445 if let Ok(small) = i32::try_from(n) {
8446 Value::Int(small)
8447 } else {
8448 Value::BigInt(n)
8449 }
8450}
8451
8452#[allow(clippy::too_many_lines)]
8458fn coerce_value(
8459 v: Value,
8460 expected: DataType,
8461 col_name: &str,
8462 position: usize,
8463) -> Result<Value, EngineError> {
8464 if v.is_null() {
8465 return Ok(Value::Null);
8466 }
8467 let actual = v.data_type().expect("non-null");
8468 if actual == expected {
8469 return Ok(v);
8470 }
8471 let coerced =
8472 match (v, expected) {
8473 (Value::Int(n), DataType::BigInt) => Some(Value::BigInt(i64::from(n))),
8474 (Value::Int(n), DataType::Float) => Some(Value::Float(f64::from(n))),
8475 (Value::Int(n), DataType::SmallInt) => i16::try_from(n).ok().map(Value::SmallInt),
8476 (Value::Int(n), DataType::Numeric { precision, scale }) => Some(numeric_from_integer(
8477 i128::from(n),
8478 precision,
8479 scale,
8480 col_name,
8481 )?),
8482 (Value::SmallInt(n), DataType::Int) => Some(Value::Int(i32::from(n))),
8483 (Value::SmallInt(n), DataType::BigInt) => Some(Value::BigInt(i64::from(n))),
8484 (Value::SmallInt(n), DataType::Float) => Some(Value::Float(f64::from(n))),
8485 (Value::SmallInt(n), DataType::Numeric { precision, scale }) => Some(
8486 numeric_from_integer(i128::from(n), precision, scale, col_name)?,
8487 ),
8488 (Value::BigInt(n), DataType::Int) => i32::try_from(n).ok().map(Value::Int),
8489 (Value::BigInt(n), DataType::SmallInt) => i16::try_from(n).ok().map(Value::SmallInt),
8490 #[allow(clippy::cast_precision_loss)]
8491 (Value::BigInt(n), DataType::Float) => Some(Value::Float(n as f64)),
8492 (Value::BigInt(n), DataType::Numeric { precision, scale }) => Some(
8493 numeric_from_integer(i128::from(n), precision, scale, col_name)?,
8494 ),
8495 (Value::Float(x), DataType::Numeric { precision, scale }) => {
8496 Some(numeric_from_float(x, precision, scale, col_name)?)
8497 }
8498 (Value::Text(s), DataType::Date) => {
8500 let d = eval::parse_date_literal(&s).ok_or_else(|| {
8501 EngineError::Eval(EvalError::TypeMismatch {
8502 detail: alloc::format!(
8503 "cannot parse {s:?} as DATE for column `{col_name}`"
8504 ),
8505 })
8506 })?;
8507 Some(Value::Date(d))
8508 }
8509 (Value::Text(s), DataType::Json | DataType::Jsonb) => Some(Value::Json(s)),
8513 (Value::Json(s), DataType::Text) => Some(Value::Text(s)),
8514 (Value::Text(s), DataType::Timestamp | DataType::Timestamptz) => {
8515 let t = eval::parse_timestamp_literal(&s).ok_or_else(|| {
8516 EngineError::Eval(EvalError::TypeMismatch {
8517 detail: alloc::format!(
8518 "cannot parse {s:?} as TIMESTAMP for column `{col_name}`"
8519 ),
8520 })
8521 })?;
8522 Some(Value::Timestamp(t))
8523 }
8524 (Value::Date(d), DataType::Timestamp | DataType::Timestamptz) => {
8527 Some(Value::Timestamp(i64::from(d) * 86_400_000_000))
8528 }
8529 (Value::Timestamp(t), DataType::Date) => {
8530 let days = t.div_euclid(86_400_000_000);
8531 i32::try_from(days).ok().map(Value::Date)
8532 }
8533 (
8534 Value::Numeric {
8535 scaled,
8536 scale: src_scale,
8537 },
8538 DataType::Numeric { precision, scale },
8539 ) => Some(numeric_rescale(
8540 scaled, src_scale, precision, scale, col_name,
8541 )?),
8542 #[allow(clippy::cast_precision_loss)]
8543 (Value::Numeric { scaled, scale }, DataType::Float) => {
8544 let mut div = 1.0_f64;
8545 for _ in 0..scale {
8546 div *= 10.0;
8547 }
8548 Some(Value::Float((scaled as f64) / div))
8549 }
8550 (Value::Numeric { scaled, scale }, DataType::Int) => {
8551 let truncated = numeric_truncate_to_integer(scaled, scale);
8552 i32::try_from(truncated).ok().map(Value::Int)
8553 }
8554 (Value::Numeric { scaled, scale }, DataType::BigInt) => {
8555 let truncated = numeric_truncate_to_integer(scaled, scale);
8556 i64::try_from(truncated).ok().map(Value::BigInt)
8557 }
8558 (Value::Numeric { scaled, scale }, DataType::SmallInt) => {
8559 let truncated = numeric_truncate_to_integer(scaled, scale);
8560 i16::try_from(truncated).ok().map(Value::SmallInt)
8561 }
8562 (Value::Text(s), DataType::Varchar(max)) => {
8564 if u32::try_from(s.chars().count()).unwrap_or(u32::MAX) <= max {
8565 Some(Value::Text(s))
8566 } else {
8567 return Err(EngineError::Unsupported(alloc::format!(
8568 "value for VARCHAR({max}) column `{col_name}` exceeds length: \
8569 {} chars",
8570 s.chars().count()
8571 )));
8572 }
8573 }
8574 (
8582 Value::Vector(v),
8583 DataType::Vector {
8584 dim,
8585 encoding: VecEncoding::Sq8,
8586 },
8587 ) if v.len() == dim as usize => {
8588 Some(Value::Sq8Vector(spg_storage::quantize::quantize(&v)))
8589 }
8590 (
8595 Value::Vector(v),
8596 DataType::Vector {
8597 dim,
8598 encoding: VecEncoding::F16,
8599 },
8600 ) if v.len() == dim as usize => Some(Value::HalfVector(
8601 spg_storage::halfvec::HalfVector::from_f32_slice(&v),
8602 )),
8603 (Value::Text(s), DataType::Char(size)) => {
8607 let len = u32::try_from(s.chars().count()).unwrap_or(u32::MAX);
8608 if len > size {
8609 return Err(EngineError::Unsupported(alloc::format!(
8610 "value for CHAR({size}) column `{col_name}` exceeds length: \
8611 {len} chars"
8612 )));
8613 }
8614 let need = (size - len) as usize;
8615 let mut padded = s;
8616 padded.reserve(need);
8617 for _ in 0..need {
8618 padded.push(' ');
8619 }
8620 Some(Value::Text(padded))
8621 }
8622 _ => None,
8623 };
8624 coerced.ok_or(EngineError::Storage(StorageError::TypeMismatch {
8625 column: col_name.into(),
8626 expected,
8627 actual,
8628 position,
8629 }))
8630}
8631
8632#[cfg(test)]
8633mod tests {
8634 use super::*;
8635 use alloc::vec;
8636
8637 fn unwrap_command_ok(r: &QueryResult) -> usize {
8638 match r {
8639 QueryResult::CommandOk { affected, .. } => *affected,
8640 QueryResult::Rows { .. } => panic!("expected CommandOk, got Rows"),
8641 }
8642 }
8643
8644 #[test]
8645 fn create_table_registers_schema() {
8646 let mut e = Engine::new();
8647 e.execute("CREATE TABLE foo (a INT NOT NULL, b TEXT)")
8648 .unwrap();
8649 assert_eq!(e.catalog().table_count(), 1);
8650 let t = e.catalog().get("foo").unwrap();
8651 assert_eq!(t.schema().columns.len(), 2);
8652 assert_eq!(t.schema().columns[0].ty, DataType::Int);
8653 assert!(!t.schema().columns[0].nullable);
8654 assert_eq!(t.schema().columns[1].ty, DataType::Text);
8655 }
8656
8657 #[test]
8658 fn create_table_vector_default_is_f32_encoded() {
8659 let mut e = Engine::new();
8660 e.execute("CREATE TABLE t (v VECTOR(8))").unwrap();
8661 let t = e.catalog().get("t").unwrap();
8662 assert_eq!(
8663 t.schema().columns[0].ty,
8664 DataType::Vector {
8665 dim: 8,
8666 encoding: VecEncoding::F32,
8667 },
8668 );
8669 }
8670
8671 #[test]
8672 fn create_table_vector_using_sq8_succeeds() {
8673 let mut e = Engine::new();
8677 e.execute("CREATE TABLE t (v VECTOR(8) USING SQ8)").unwrap();
8678 let t = e.catalog().get("t").unwrap();
8679 assert_eq!(
8680 t.schema().columns[0].ty,
8681 DataType::Vector {
8682 dim: 8,
8683 encoding: VecEncoding::Sq8,
8684 },
8685 );
8686 }
8687
8688 #[test]
8689 fn insert_into_sq8_column_quantises_f32_payload() {
8690 let mut e = Engine::new();
8697 e.execute("CREATE TABLE t (v VECTOR(4) USING SQ8)").unwrap();
8698 e.execute("INSERT INTO t VALUES ([0.0, 0.25, 0.5, 1.0])")
8699 .unwrap();
8700 let t = e.catalog().get("t").unwrap();
8701 assert_eq!(t.rows().len(), 1);
8702 match &t.rows()[0].values[0] {
8703 Value::Sq8Vector(q) => {
8704 assert_eq!(q.bytes.len(), 4);
8705 assert!((q.min - 0.0).abs() < 1e-6);
8707 assert!((q.max - 1.0).abs() < 1e-6);
8708 }
8709 other => panic!("expected Sq8Vector cell, got {other:?}"),
8710 }
8711 }
8712
8713 #[test]
8714 fn create_table_vector_using_half_succeeds_and_insert_converts_to_f16() {
8715 let mut e = Engine::new();
8722 e.execute("CREATE TABLE t (v VECTOR(4) USING HALF)")
8723 .unwrap();
8724 e.execute("INSERT INTO t VALUES ([0.0, 0.25, 0.5, 1.0])")
8725 .unwrap();
8726 let t = e.catalog().get("t").unwrap();
8727 assert_eq!(t.rows().len(), 1);
8728 match &t.rows()[0].values[0] {
8729 Value::HalfVector(h) => {
8730 assert_eq!(h.dim(), 4);
8731 let back = h.to_f32_vec();
8732 let expected = alloc::vec![0.0_f32, 0.25, 0.5, 1.0];
8733 for (g, e) in back.iter().zip(expected.iter()) {
8734 assert!(
8735 (g - e).abs() < 1e-6,
8736 "{g} vs {e} should be exact on f16 grid"
8737 );
8738 }
8739 }
8740 other => panic!("expected HalfVector cell, got {other:?}"),
8741 }
8742 }
8743
8744 #[test]
8745 fn alter_index_rebuild_in_place_succeeds() {
8746 let mut e = Engine::new();
8751 e.execute("CREATE TABLE t (id INT NOT NULL, v VECTOR(3) NOT NULL)")
8752 .unwrap();
8753 for i in 0..8_i32 {
8754 #[allow(clippy::cast_precision_loss)]
8755 let base = (i as f32) * 0.1;
8756 e.execute(&alloc::format!(
8757 "INSERT INTO t VALUES ({i}, [{base}, {b1}, {b2}])",
8758 b1 = base + 0.01,
8759 b2 = base + 0.02,
8760 ))
8761 .unwrap();
8762 }
8763 e.execute("CREATE INDEX t_idx ON t USING hnsw (v)").unwrap();
8764 e.execute("ALTER INDEX t_idx REBUILD").unwrap();
8765 assert_eq!(
8767 e.catalog().get("t").unwrap().schema().columns[1].ty,
8768 DataType::Vector {
8769 dim: 3,
8770 encoding: VecEncoding::F32,
8771 },
8772 );
8773 }
8774
8775 #[test]
8776 fn alter_index_rebuild_with_encoding_switches_cell_type() {
8777 let mut e = Engine::new();
8782 e.execute("CREATE TABLE t (id INT NOT NULL, v VECTOR(4) NOT NULL)")
8783 .unwrap();
8784 e.execute("INSERT INTO t VALUES (1, [0.0, 0.25, 0.5, 1.0])")
8785 .unwrap();
8786 e.execute("CREATE INDEX t_idx ON t USING hnsw (v)").unwrap();
8787 e.execute("ALTER INDEX t_idx REBUILD WITH (encoding = SQ8)")
8788 .unwrap();
8789 let t = e.catalog().get("t").unwrap();
8790 assert_eq!(
8791 t.schema().columns[1].ty,
8792 DataType::Vector {
8793 dim: 4,
8794 encoding: VecEncoding::Sq8,
8795 },
8796 );
8797 assert!(matches!(t.rows()[0].values[1], Value::Sq8Vector(_)));
8798 }
8799
8800 #[test]
8801 fn alter_index_rebuild_unknown_index_errors() {
8802 let mut e = Engine::new();
8803 let err = e.execute("ALTER INDEX nope REBUILD").unwrap_err();
8804 assert!(
8805 matches!(
8806 &err,
8807 EngineError::Storage(StorageError::IndexNotFound { name }) if name == "nope"
8808 ),
8809 "got: {err}"
8810 );
8811 }
8812
8813 #[test]
8814 fn alter_index_rebuild_on_btree_index_errors() {
8815 let mut e = Engine::new();
8818 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
8819 e.execute("INSERT INTO t VALUES (1)").unwrap();
8820 e.execute("CREATE INDEX t_idx ON t (id)").unwrap();
8821 let err = e.execute("ALTER INDEX t_idx REBUILD").unwrap_err();
8822 assert!(
8823 matches!(&err, EngineError::Storage(StorageError::Unsupported(_))),
8824 "got: {err}"
8825 );
8826 }
8827
8828 #[test]
8829 fn prepared_insert_substitutes_placeholders() {
8830 let mut e = Engine::new();
8836 e.execute("CREATE TABLE t (id INT NOT NULL, name TEXT NOT NULL)")
8837 .unwrap();
8838 let stmt = e.prepare("INSERT INTO t VALUES ($1, $2)").unwrap();
8839 for (id, name) in [(1, "alice"), (2, "bob"), (3, "carol")] {
8840 e.execute_prepared(
8841 stmt.clone(),
8842 &[Value::Int(id), Value::Text(name.into())],
8843 )
8844 .unwrap();
8845 }
8846 let rows_result = e.execute("SELECT id, name FROM t").unwrap();
8848 let QueryResult::Rows { rows, .. } = rows_result else {
8849 panic!("expected Rows")
8850 };
8851 assert_eq!(rows.len(), 3);
8852 }
8853
8854 #[test]
8855 fn prepared_select_with_placeholder_filters_rows() {
8856 let mut e = Engine::new();
8857 e.execute("CREATE TABLE t (id INT NOT NULL, v INT NOT NULL)")
8858 .unwrap();
8859 for i in 0..10_i32 {
8860 e.execute(&alloc::format!("INSERT INTO t VALUES ({i}, {})", i * 7))
8861 .unwrap();
8862 }
8863 let stmt = e
8864 .prepare("SELECT id FROM t WHERE v = $1")
8865 .unwrap();
8866 let QueryResult::Rows { rows, .. } = e
8867 .execute_prepared(stmt, &[Value::Int(35)])
8868 .unwrap()
8869 else {
8870 panic!("expected Rows")
8871 };
8872 assert_eq!(rows.len(), 1);
8874 assert_eq!(rows[0].values[0], Value::Int(5));
8875 }
8876
8877 #[test]
8878 fn prepared_too_few_params_errors() {
8879 let mut e = Engine::new();
8880 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
8881 let stmt = e.prepare("INSERT INTO t VALUES ($1)").unwrap();
8882 let err = e.execute_prepared(stmt, &[]).unwrap_err();
8883 assert!(
8884 matches!(
8885 &err,
8886 EngineError::Eval(EvalError::PlaceholderOutOfRange { n: 1, bound: 0 })
8887 ),
8888 "got: {err}"
8889 );
8890 }
8891
8892 #[test]
8893 fn insert_into_half_column_dim_mismatch_errors() {
8894 let mut e = Engine::new();
8895 e.execute("CREATE TABLE t (v VECTOR(4) USING HALF)")
8896 .unwrap();
8897 let err = e.execute("INSERT INTO t VALUES ([1.0, 2.0])").unwrap_err();
8898 assert!(matches!(
8899 &err,
8900 EngineError::Storage(StorageError::TypeMismatch { .. })
8901 ));
8902 }
8903
8904 #[test]
8905 fn insert_into_sq8_column_dim_mismatch_errors() {
8906 let mut e = Engine::new();
8911 e.execute("CREATE TABLE t (v VECTOR(4) USING SQ8)").unwrap();
8912 let err = e.execute("INSERT INTO t VALUES ([1.0, 2.0])").unwrap_err();
8913 assert!(
8914 matches!(
8915 &err,
8916 EngineError::Storage(StorageError::TypeMismatch { .. })
8917 ),
8918 "got: {err}",
8919 );
8920 }
8921
8922 #[test]
8923 fn create_table_duplicate_errors() {
8924 let mut e = Engine::new();
8925 e.execute("CREATE TABLE foo (a INT)").unwrap();
8926 let err = e.execute("CREATE TABLE foo (a INT)").unwrap_err();
8927 assert!(matches!(
8928 err,
8929 EngineError::Storage(StorageError::DuplicateTable { ref name }) if name == "foo"
8930 ));
8931 }
8932
8933 #[test]
8934 fn insert_into_unknown_table_errors() {
8935 let mut e = Engine::new();
8936 let err = e.execute("INSERT INTO ghost VALUES (1)").unwrap_err();
8937 assert!(matches!(
8938 err,
8939 EngineError::Storage(StorageError::TableNotFound { ref name }) if name == "ghost"
8940 ));
8941 }
8942
8943 #[test]
8944 fn insert_happy_path_reports_one_affected() {
8945 let mut e = Engine::new();
8946 e.execute("CREATE TABLE foo (a INT NOT NULL)").unwrap();
8947 let r = e.execute("INSERT INTO foo VALUES (42)").unwrap();
8948 assert_eq!(unwrap_command_ok(&r), 1);
8949 assert_eq!(e.catalog().get("foo").unwrap().row_count(), 1);
8950 }
8951
8952 #[test]
8953 fn insert_arity_mismatch_propagates() {
8954 let mut e = Engine::new();
8955 e.execute("CREATE TABLE foo (a INT, b TEXT)").unwrap();
8956 let err = e.execute("INSERT INTO foo VALUES (1)").unwrap_err();
8957 assert!(matches!(
8958 err,
8959 EngineError::Storage(StorageError::ArityMismatch { .. })
8960 ));
8961 }
8962
8963 #[test]
8964 fn insert_negative_integer_via_unary_minus() {
8965 let mut e = Engine::new();
8966 e.execute("CREATE TABLE foo (a INT NOT NULL)").unwrap();
8967 e.execute("INSERT INTO foo VALUES (-7)").unwrap();
8968 let rows = e.catalog().get("foo").unwrap().rows();
8969 assert_eq!(rows[0].values[0], Value::Int(-7));
8970 }
8971
8972 #[test]
8973 fn insert_non_literal_expr_unsupported() {
8974 let mut e = Engine::new();
8975 e.execute("CREATE TABLE foo (a INT NOT NULL)").unwrap();
8976 let err = e.execute("INSERT INTO foo VALUES (1 + 2)").unwrap_err();
8977 assert!(matches!(err, EngineError::Unsupported(_)));
8978 }
8979
8980 #[test]
8981 fn select_star_returns_all_rows_in_insertion_order() {
8982 let mut e = Engine::new();
8983 e.execute("CREATE TABLE foo (a INT NOT NULL, b TEXT NOT NULL)")
8984 .unwrap();
8985 e.execute("INSERT INTO foo VALUES (1, 'one')").unwrap();
8986 e.execute("INSERT INTO foo VALUES (2, 'two')").unwrap();
8987 e.execute("INSERT INTO foo VALUES (3, 'three')").unwrap();
8988
8989 let r = e.execute("SELECT * FROM foo").unwrap();
8990 let QueryResult::Rows { columns, rows } = r else {
8991 panic!("expected Rows")
8992 };
8993 assert_eq!(columns.len(), 2);
8994 assert_eq!(columns[0].name, "a");
8995 assert_eq!(rows.len(), 3);
8996 assert_eq!(
8997 rows[1].values,
8998 vec![Value::Int(2), Value::Text("two".into())]
8999 );
9000 }
9001
9002 #[test]
9003 fn select_star_on_empty_table_returns_zero_rows() {
9004 let mut e = Engine::new();
9005 e.execute("CREATE TABLE foo (a INT)").unwrap();
9006 let r = e.execute("SELECT * FROM foo").unwrap();
9007 match r {
9008 QueryResult::Rows { rows, .. } => assert!(rows.is_empty()),
9009 QueryResult::CommandOk { .. } => panic!("expected Rows"),
9010 }
9011 }
9012
9013 fn make_three_row_users(e: &mut Engine) {
9016 e.execute("CREATE TABLE users (id INT NOT NULL, name TEXT NOT NULL, score INT)")
9017 .unwrap();
9018 e.execute("INSERT INTO users VALUES (1, 'alice', 90)")
9019 .unwrap();
9020 e.execute("INSERT INTO users VALUES (2, 'bob', NULL)")
9021 .unwrap();
9022 e.execute("INSERT INTO users VALUES (3, 'cara', 70)")
9023 .unwrap();
9024 }
9025
9026 fn unwrap_rows(r: QueryResult) -> (Vec<ColumnSchema>, Vec<Row>) {
9027 match r {
9028 QueryResult::Rows { columns, rows } => (columns, rows),
9029 QueryResult::CommandOk { .. } => panic!("expected Rows"),
9030 }
9031 }
9032
9033 #[test]
9034 fn where_filter_passes_only_true_rows() {
9035 let mut e = Engine::new();
9036 make_three_row_users(&mut e);
9037 let r = e.execute("SELECT * FROM users WHERE id > 1").unwrap();
9038 let (_, rows) = unwrap_rows(r);
9039 assert_eq!(rows.len(), 2);
9040 assert_eq!(rows[0].values[0], Value::Int(2));
9041 assert_eq!(rows[1].values[0], Value::Int(3));
9042 }
9043
9044 #[test]
9045 fn where_with_null_result_filters_out_row() {
9046 let mut e = Engine::new();
9047 make_three_row_users(&mut e);
9048 let r = e.execute("SELECT * FROM users WHERE score > 80").unwrap();
9050 let (_, rows) = unwrap_rows(r);
9051 assert_eq!(rows.len(), 1);
9052 assert_eq!(rows[0].values[1], Value::Text("alice".into()));
9053 }
9054
9055 #[test]
9056 fn projection_named_columns() {
9057 let mut e = Engine::new();
9058 make_three_row_users(&mut e);
9059 let r = e.execute("SELECT name, score FROM users").unwrap();
9060 let (cols, rows) = unwrap_rows(r);
9061 assert_eq!(cols.len(), 2);
9062 assert_eq!(cols[0].name, "name");
9063 assert_eq!(cols[1].name, "score");
9064 assert_eq!(rows.len(), 3);
9065 assert_eq!(
9066 rows[0].values,
9067 vec![Value::Text("alice".into()), Value::Int(90)]
9068 );
9069 }
9070
9071 #[test]
9072 fn projection_with_column_alias() {
9073 let mut e = Engine::new();
9074 make_three_row_users(&mut e);
9075 let r = e
9076 .execute("SELECT name AS who FROM users WHERE id = 1")
9077 .unwrap();
9078 let (cols, rows) = unwrap_rows(r);
9079 assert_eq!(cols[0].name, "who");
9080 assert_eq!(rows.len(), 1);
9081 assert_eq!(rows[0].values[0], Value::Text("alice".into()));
9082 }
9083
9084 #[test]
9085 fn qualified_column_with_table_alias_resolves() {
9086 let mut e = Engine::new();
9087 make_three_row_users(&mut e);
9088 let r = e
9089 .execute("SELECT u.id, u.name FROM users AS u WHERE u.id < 3")
9090 .unwrap();
9091 let (cols, rows) = unwrap_rows(r);
9092 assert_eq!(cols.len(), 2);
9093 assert_eq!(rows.len(), 2);
9094 }
9095
9096 #[test]
9097 fn qualified_column_with_wrong_alias_errors() {
9098 let mut e = Engine::new();
9099 make_three_row_users(&mut e);
9100 let err = e.execute("SELECT x.id FROM users AS u").unwrap_err();
9101 assert!(matches!(
9102 err,
9103 EngineError::Eval(EvalError::UnknownQualifier { ref qualifier }) if qualifier == "x"
9104 ));
9105 }
9106
9107 #[test]
9108 fn select_unknown_column_errors_in_projection() {
9109 let mut e = Engine::new();
9110 make_three_row_users(&mut e);
9111 let err = e.execute("SELECT ghost FROM users").unwrap_err();
9112 assert!(matches!(
9113 err,
9114 EngineError::Eval(EvalError::ColumnNotFound { ref name }) if name == "ghost"
9115 ));
9116 }
9117
9118 #[test]
9119 fn where_unknown_column_errors() {
9120 let mut e = Engine::new();
9121 make_three_row_users(&mut e);
9122 let err = e
9123 .execute("SELECT * FROM users WHERE ghost = 1")
9124 .unwrap_err();
9125 assert!(matches!(
9126 err,
9127 EngineError::Eval(EvalError::ColumnNotFound { .. })
9128 ));
9129 }
9130
9131 #[test]
9132 fn expression_projection_evaluates_and_renders() {
9133 let mut e = Engine::new();
9136 e.execute("CREATE TABLE t (a INT NOT NULL)").unwrap();
9137 e.execute("INSERT INTO t VALUES (3)").unwrap();
9138 let (_, rows) = unwrap_rows(e.execute("SELECT 1 + 2 FROM t").unwrap());
9139 assert_eq!(rows.len(), 1);
9140 assert_eq!(rows[0].values[0], Value::Int(3));
9143 }
9144
9145 #[test]
9146 fn select_unknown_table_errors() {
9147 let mut e = Engine::new();
9148 let err = e.execute("SELECT * FROM ghost").unwrap_err();
9149 assert!(matches!(
9150 err,
9151 EngineError::Storage(StorageError::TableNotFound { .. })
9152 ));
9153 }
9154
9155 #[test]
9156 fn invalid_sql_returns_parse_error() {
9157 let mut e = Engine::new();
9160 let err = e.execute("THIS_IS_NOT_A_KEYWORD foo bar baz").unwrap_err();
9161 assert!(matches!(err, EngineError::Parse(_)));
9162 }
9163
9164 #[test]
9167 fn create_index_registers_on_table() {
9168 let mut e = Engine::new();
9169 make_three_row_users(&mut e);
9170 e.execute("CREATE INDEX by_name ON users (name)").unwrap();
9171 let t = e.catalog().get("users").unwrap();
9172 assert_eq!(t.indices().len(), 1);
9173 assert_eq!(t.indices()[0].name, "by_name");
9174 }
9175
9176 #[test]
9177 fn create_index_on_unknown_table_errors() {
9178 let mut e = Engine::new();
9179 let err = e.execute("CREATE INDEX i ON ghost (a)").unwrap_err();
9180 assert!(matches!(
9181 err,
9182 EngineError::Storage(StorageError::TableNotFound { .. })
9183 ));
9184 }
9185
9186 #[test]
9187 fn create_index_on_unknown_column_errors() {
9188 let mut e = Engine::new();
9189 make_three_row_users(&mut e);
9190 let err = e.execute("CREATE INDEX i ON users (ghost)").unwrap_err();
9191 assert!(matches!(
9192 err,
9193 EngineError::Storage(StorageError::ColumnNotFound { .. })
9194 ));
9195 }
9196
9197 #[test]
9198 fn select_eq_uses_index_returns_same_rows_as_scan() {
9199 let mut without = Engine::new();
9203 make_three_row_users(&mut without);
9204 let mut with = Engine::new();
9205 make_three_row_users(&mut with);
9206 with.execute("CREATE INDEX by_id ON users (id)").unwrap();
9207
9208 let q = "SELECT * FROM users WHERE id = 2";
9209 let (_, no_idx_rows) = unwrap_rows(without.execute(q).unwrap());
9210 let (_, idx_rows) = unwrap_rows(with.execute(q).unwrap());
9211 assert_eq!(no_idx_rows, idx_rows);
9212 assert_eq!(idx_rows.len(), 1);
9213 }
9214
9215 #[test]
9216 fn select_eq_with_no_matching_index_value_returns_empty() {
9217 let mut e = Engine::new();
9218 make_three_row_users(&mut e);
9219 e.execute("CREATE INDEX by_id ON users (id)").unwrap();
9220 let (_, rows) = unwrap_rows(e.execute("SELECT * FROM users WHERE id = 999").unwrap());
9221 assert_eq!(rows.len(), 0);
9222 }
9223
9224 #[test]
9227 fn begin_sets_in_transaction_flag() {
9228 let mut e = Engine::new();
9229 assert!(!e.in_transaction());
9230 e.execute("BEGIN").unwrap();
9231 assert!(e.in_transaction());
9232 }
9233
9234 #[test]
9235 fn double_begin_errors() {
9236 let mut e = Engine::new();
9237 e.execute("BEGIN").unwrap();
9238 let err = e.execute("BEGIN").unwrap_err();
9239 assert_eq!(err, EngineError::TransactionAlreadyOpen);
9240 }
9241
9242 #[test]
9243 fn commit_without_begin_errors() {
9244 let mut e = Engine::new();
9245 let err = e.execute("COMMIT").unwrap_err();
9246 assert_eq!(err, EngineError::NoActiveTransaction);
9247 }
9248
9249 #[test]
9250 fn rollback_without_begin_errors() {
9251 let mut e = Engine::new();
9252 let err = e.execute("ROLLBACK").unwrap_err();
9253 assert_eq!(err, EngineError::NoActiveTransaction);
9254 }
9255
9256 #[test]
9257 fn commit_applies_shadow_to_committed_catalog() {
9258 let mut e = Engine::new();
9259 e.execute("CREATE TABLE t (v INT NOT NULL)").unwrap();
9260 e.execute("BEGIN").unwrap();
9261 e.execute("INSERT INTO t VALUES (1)").unwrap();
9262 e.execute("INSERT INTO t VALUES (2)").unwrap();
9263 e.execute("COMMIT").unwrap();
9264 assert!(!e.in_transaction());
9265 assert_eq!(e.catalog().get("t").unwrap().row_count(), 2);
9266 }
9267
9268 #[test]
9269 fn rollback_discards_shadow() {
9270 let mut e = Engine::new();
9271 e.execute("CREATE TABLE t (v INT NOT NULL)").unwrap();
9272 e.execute("BEGIN").unwrap();
9273 e.execute("INSERT INTO t VALUES (1)").unwrap();
9274 e.execute("INSERT INTO t VALUES (2)").unwrap();
9275 e.execute("ROLLBACK").unwrap();
9276 assert!(!e.in_transaction());
9277 assert_eq!(e.catalog().get("t").unwrap().row_count(), 0);
9278 }
9279
9280 #[test]
9281 fn select_during_tx_sees_uncommitted_writes_own_session() {
9282 let mut e = Engine::new();
9285 e.execute("CREATE TABLE t (v INT NOT NULL)").unwrap();
9286 e.execute("BEGIN").unwrap();
9287 e.execute("INSERT INTO t VALUES (42)").unwrap();
9288 let (_, rows) = unwrap_rows(e.execute("SELECT * FROM t").unwrap());
9289 assert_eq!(rows.len(), 1);
9290 assert_eq!(rows[0].values[0], Value::Int(42));
9291 }
9292
9293 #[test]
9294 fn snapshot_with_no_users_is_bare_catalog_format() {
9295 let mut e = Engine::new();
9296 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9297 let bytes = e.snapshot();
9298 assert_eq!(
9299 &bytes[..8],
9300 b"SPGDB001",
9301 "must be the bare v3.x catalog magic"
9302 );
9303 let e2 = Engine::restore_envelope(&bytes).unwrap();
9304 assert!(e2.users().is_empty());
9305 assert_eq!(e2.catalog().table_count(), 1);
9306 }
9307
9308 #[test]
9309 fn snapshot_with_users_round_trips_both_via_envelope() {
9310 let mut e = Engine::new();
9311 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9312 e.create_user("alice", "pw1", Role::Admin, [9; 16]).unwrap();
9313 e.create_user("bob", "pw2", Role::ReadOnly, [5; 16])
9314 .unwrap();
9315 let bytes = e.snapshot();
9316 assert_eq!(&bytes[..8], b"SPGENV01", "must be the v4.1 envelope magic");
9317 let e2 = Engine::restore_envelope(&bytes).unwrap();
9318 assert_eq!(e2.users().len(), 2);
9319 assert_eq!(e2.verify_user("alice", "pw1"), Some(Role::Admin));
9320 assert_eq!(e2.verify_user("bob", "pw2"), Some(Role::ReadOnly));
9321 assert_eq!(e2.verify_user("alice", "wrong"), None);
9322 assert_eq!(e2.catalog().table_count(), 1);
9323 }
9324
9325 #[test]
9326 fn ddl_inside_tx_also_rolled_back() {
9327 let mut e = Engine::new();
9328 e.execute("BEGIN").unwrap();
9329 e.execute("CREATE TABLE t (v INT)").unwrap();
9330 e.execute("SELECT * FROM t").unwrap();
9332 e.execute("ROLLBACK").unwrap();
9333 let err = e.execute("SELECT * FROM t").unwrap_err();
9335 assert!(matches!(
9336 err,
9337 EngineError::Storage(StorageError::TableNotFound { .. })
9338 ));
9339 }
9340
9341 #[test]
9344 fn create_publication_lands_in_catalog() {
9345 let mut e = Engine::new();
9346 assert!(e.publications().is_empty());
9347 e.execute("CREATE PUBLICATION pub_a").unwrap();
9348 assert_eq!(e.publications().len(), 1);
9349 assert!(e.publications().contains("pub_a"));
9350 }
9351
9352 #[test]
9353 fn create_publication_duplicate_errors() {
9354 let mut e = Engine::new();
9355 e.execute("CREATE PUBLICATION pub_a").unwrap();
9356 let err = e.execute("CREATE PUBLICATION pub_a").unwrap_err();
9357 assert!(
9358 alloc::format!("{err:?}").contains("DuplicateName"),
9359 "got {err:?}"
9360 );
9361 }
9362
9363 #[test]
9364 fn drop_publication_silent_when_absent() {
9365 let mut e = Engine::new();
9366 let r = e.execute("DROP PUBLICATION nope").unwrap();
9369 match r {
9370 QueryResult::CommandOk { affected, .. } => assert_eq!(affected, 0),
9371 other => panic!("expected CommandOk, got {other:?}"),
9372 }
9373 }
9374
9375 #[test]
9376 fn drop_publication_present_reports_one_affected() {
9377 let mut e = Engine::new();
9378 e.execute("CREATE PUBLICATION pub_a").unwrap();
9379 let r = e.execute("DROP PUBLICATION pub_a").unwrap();
9380 match r {
9381 QueryResult::CommandOk {
9382 affected,
9383 modified_catalog,
9384 } => {
9385 assert_eq!(affected, 1);
9386 assert!(modified_catalog);
9387 }
9388 other => panic!("expected CommandOk, got {other:?}"),
9389 }
9390 assert!(e.publications().is_empty());
9391 }
9392
9393 #[test]
9394 fn publications_persist_across_snapshot_restore() {
9395 let mut e = Engine::new();
9400 e.execute("CREATE PUBLICATION pub_a").unwrap();
9401 e.execute("CREATE PUBLICATION pub_b FOR ALL TABLES").unwrap();
9402 let snap = e.snapshot();
9403 let e2 = Engine::restore_envelope(&snap).unwrap();
9404 assert_eq!(e2.publications().len(), 2);
9405 assert!(e2.publications().contains("pub_a"));
9406 assert!(e2.publications().contains("pub_b"));
9407 }
9408
9409 #[test]
9410 fn create_publication_allowed_inside_transaction() {
9411 let mut e = Engine::new();
9415 e.execute("BEGIN").unwrap();
9416 e.execute("CREATE PUBLICATION pub_a").unwrap();
9417 e.execute("COMMIT").unwrap();
9418 assert!(e.publications().contains("pub_a"));
9419 }
9420
9421 #[test]
9424 fn create_publication_for_table_list_lands_with_scope() {
9425 let mut e = Engine::new();
9426 e.execute("CREATE TABLE t1 (id INT NOT NULL)").unwrap();
9427 e.execute("CREATE TABLE t2 (id INT NOT NULL)").unwrap();
9428 e.execute("CREATE PUBLICATION pub_a FOR TABLE t1, t2")
9429 .unwrap();
9430 let scope = e.publications().get("pub_a").cloned();
9431 let Some(spg_sql::ast::PublicationScope::ForTables(ts)) = scope else {
9432 panic!("expected ForTables scope, got {scope:?}")
9433 };
9434 assert_eq!(ts, alloc::vec!["t1".to_string(), "t2".to_string()]);
9435 }
9436
9437 #[test]
9438 fn create_publication_all_tables_except_lands_with_scope() {
9439 let mut e = Engine::new();
9440 e.execute("CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t3")
9441 .unwrap();
9442 let scope = e.publications().get("pub_a").cloned();
9443 let Some(spg_sql::ast::PublicationScope::AllTablesExcept(ts)) = scope else {
9444 panic!("expected AllTablesExcept scope, got {scope:?}")
9445 };
9446 assert_eq!(ts, alloc::vec!["t3".to_string()]);
9447 }
9448
9449 #[test]
9450 fn show_publications_empty_returns_zero_rows() {
9451 let e = Engine::new();
9452 let r = e.execute_readonly("SHOW PUBLICATIONS").unwrap();
9453 let QueryResult::Rows { rows, columns } = r else {
9454 panic!()
9455 };
9456 assert!(rows.is_empty());
9457 assert_eq!(columns.len(), 3);
9458 assert_eq!(columns[0].name, "name");
9459 assert_eq!(columns[1].name, "scope");
9460 assert_eq!(columns[2].name, "table_count");
9461 }
9462
9463 #[test]
9464 fn show_publications_returns_one_row_per_publication_ordered_by_name() {
9465 let mut e = Engine::new();
9466 e.execute("CREATE PUBLICATION z_pub").unwrap();
9467 e.execute("CREATE PUBLICATION a_pub FOR TABLE t1, t2")
9468 .unwrap();
9469 e.execute("CREATE PUBLICATION m_pub FOR ALL TABLES EXCEPT bad")
9470 .unwrap();
9471 let r = e.execute_readonly("SHOW PUBLICATIONS").unwrap();
9472 let QueryResult::Rows { rows, .. } = r else {
9473 panic!()
9474 };
9475 assert_eq!(rows.len(), 3);
9476 let names: Vec<&str> = rows
9478 .iter()
9479 .map(|r| {
9480 if let Value::Text(s) = &r.values[0] {
9481 s.as_str()
9482 } else {
9483 panic!()
9484 }
9485 })
9486 .collect();
9487 assert_eq!(names, alloc::vec!["a_pub", "m_pub", "z_pub"]);
9488 match &rows[0].values[1] {
9490 Value::Text(s) => assert_eq!(s, "FOR TABLE t1, t2"),
9491 other => panic!("expected Text, got {other:?}"),
9492 }
9493 assert_eq!(rows[0].values[2], Value::Int(2));
9494 match &rows[1].values[1] {
9496 Value::Text(s) => assert_eq!(s, "FOR ALL TABLES EXCEPT bad"),
9497 other => panic!("expected Text, got {other:?}"),
9498 }
9499 assert_eq!(rows[1].values[2], Value::Int(1));
9500 match &rows[2].values[1] {
9502 Value::Text(s) => assert_eq!(s, "FOR ALL TABLES"),
9503 other => panic!("expected Text, got {other:?}"),
9504 }
9505 assert_eq!(rows[2].values[2], Value::Null);
9506 }
9507
9508 #[test]
9509 fn for_list_scopes_persist_across_snapshot() {
9510 let mut e = Engine::new();
9513 e.execute("CREATE PUBLICATION p1 FOR TABLE t1, t2").unwrap();
9514 e.execute("CREATE PUBLICATION p2 FOR ALL TABLES EXCEPT bad, worse")
9515 .unwrap();
9516 let snap = e.snapshot();
9517 let e2 = Engine::restore_envelope(&snap).unwrap();
9518 assert_eq!(e2.publications().len(), 2);
9519 let p1 = e2.publications().get("p1").cloned();
9520 let Some(spg_sql::ast::PublicationScope::ForTables(ts)) = p1 else {
9521 panic!("p1 scope lost: {p1:?}")
9522 };
9523 assert_eq!(ts, alloc::vec!["t1".to_string(), "t2".to_string()]);
9524 let p2 = e2.publications().get("p2").cloned();
9525 let Some(spg_sql::ast::PublicationScope::AllTablesExcept(ts)) = p2 else {
9526 panic!("p2 scope lost: {p2:?}")
9527 };
9528 assert_eq!(ts, alloc::vec!["bad".to_string(), "worse".to_string()]);
9529 }
9530
9531 #[test]
9534 fn create_subscription_lands_in_catalog_with_defaults() {
9535 let mut e = Engine::new();
9536 e.execute(
9537 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
9538 )
9539 .unwrap();
9540 let s = e.subscriptions().get("sub_a").cloned().expect("present");
9541 assert_eq!(s.conn_str, "host=127.0.0.1 port=20002");
9542 assert_eq!(s.publications, alloc::vec!["pub_a".to_string()]);
9543 assert!(s.enabled);
9544 assert_eq!(s.last_received_pos, 0);
9545 }
9546
9547 #[test]
9548 fn create_subscription_duplicate_name_errors() {
9549 let mut e = Engine::new();
9550 e.execute("CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p")
9551 .unwrap();
9552 let err = e
9553 .execute("CREATE SUBSCRIPTION s CONNECTION 'host=y' PUBLICATION p")
9554 .unwrap_err();
9555 assert!(
9556 alloc::format!("{err:?}").contains("DuplicateName"),
9557 "got {err:?}"
9558 );
9559 }
9560
9561 #[test]
9562 fn drop_subscription_silent_when_absent() {
9563 let mut e = Engine::new();
9564 let r = e.execute("DROP SUBSCRIPTION never").unwrap();
9565 match r {
9566 QueryResult::CommandOk { affected, .. } => assert_eq!(affected, 0),
9567 other => panic!("expected CommandOk, got {other:?}"),
9568 }
9569 }
9570
9571 #[test]
9572 fn subscription_advance_updates_last_pos_monotone() {
9573 let mut e = Engine::new();
9574 e.execute("CREATE SUBSCRIPTION s CONNECTION 'h=x' PUBLICATION p")
9575 .unwrap();
9576 assert!(e.subscription_advance("s", 100));
9577 assert_eq!(e.subscriptions().get("s").unwrap().last_received_pos, 100);
9578 assert!(e.subscription_advance("s", 50)); assert_eq!(e.subscriptions().get("s").unwrap().last_received_pos, 100);
9580 assert!(e.subscription_advance("s", 200));
9581 assert_eq!(e.subscriptions().get("s").unwrap().last_received_pos, 200);
9582 assert!(!e.subscription_advance("missing", 1));
9583 }
9584
9585 #[test]
9586 fn show_subscriptions_returns_rows_ordered_by_name() {
9587 let mut e = Engine::new();
9588 e.execute("CREATE SUBSCRIPTION z_sub CONNECTION 'h=x' PUBLICATION p1, p2")
9589 .unwrap();
9590 e.execute("CREATE SUBSCRIPTION a_sub CONNECTION 'h=y' PUBLICATION p3")
9591 .unwrap();
9592 let r = e.execute_readonly("SHOW SUBSCRIPTIONS").unwrap();
9593 let QueryResult::Rows { rows, columns } = r else {
9594 panic!()
9595 };
9596 assert_eq!(rows.len(), 2);
9597 assert_eq!(columns.len(), 5);
9598 assert_eq!(columns[0].name, "name");
9599 assert_eq!(columns[4].name, "last_received_pos");
9600 let names: Vec<&str> = rows
9602 .iter()
9603 .map(|r| {
9604 if let Value::Text(s) = &r.values[0] {
9605 s.as_str()
9606 } else {
9607 panic!()
9608 }
9609 })
9610 .collect();
9611 assert_eq!(names, alloc::vec!["a_sub", "z_sub"]);
9612 assert_eq!(rows[0].values[1], Value::Text("h=y".to_string()));
9614 assert_eq!(rows[0].values[2], Value::Text("p3".to_string()));
9615 assert_eq!(rows[0].values[3], Value::Bool(true));
9616 assert_eq!(rows[0].values[4], Value::BigInt(0));
9617 assert_eq!(rows[1].values[2], Value::Text("p1, p2".to_string()));
9619 }
9620
9621 #[test]
9622 fn subscriptions_persist_across_snapshot_envelope_v4() {
9623 let mut e = Engine::new();
9624 e.execute("CREATE SUBSCRIPTION s1 CONNECTION 'h=A' PUBLICATION p1, p2")
9625 .unwrap();
9626 e.execute("CREATE SUBSCRIPTION s2 CONNECTION 'h=B' PUBLICATION p3")
9627 .unwrap();
9628 e.subscription_advance("s2", 42);
9629 let snap = e.snapshot();
9630 let e2 = Engine::restore_envelope(&snap).unwrap();
9631 assert_eq!(e2.subscriptions().len(), 2);
9632 let s1 = e2.subscriptions().get("s1").unwrap();
9633 assert_eq!(s1.conn_str, "h=A");
9634 assert_eq!(s1.publications, alloc::vec!["p1".to_string(), "p2".to_string()]);
9635 assert_eq!(s1.last_received_pos, 0);
9636 let s2 = e2.subscriptions().get("s2").unwrap();
9637 assert_eq!(s2.last_received_pos, 42);
9638 }
9639
9640 #[test]
9641 fn v3_envelope_loads_with_empty_subscriptions() {
9642 let mut e = Engine::new();
9646 e.execute("CREATE PUBLICATION pub_legacy").unwrap();
9647 let catalog = e.catalog.serialize();
9648 let users = crate::users::serialize_users(&e.users);
9649 let pubs = e.publications.serialize();
9650 let mut buf = Vec::new();
9651 buf.extend_from_slice(b"SPGENV01");
9652 buf.push(3u8); buf.extend_from_slice(&u32::try_from(catalog.len()).unwrap().to_le_bytes());
9654 buf.extend_from_slice(&catalog);
9655 buf.extend_from_slice(&u32::try_from(users.len()).unwrap().to_le_bytes());
9656 buf.extend_from_slice(&users);
9657 buf.extend_from_slice(&u32::try_from(pubs.len()).unwrap().to_le_bytes());
9658 buf.extend_from_slice(&pubs);
9659 let crc = spg_crypto::crc32::crc32(&buf);
9660 buf.extend_from_slice(&crc.to_le_bytes());
9661
9662 let e2 = Engine::restore_envelope(&buf).expect("v3 envelope restores under v4 reader");
9663 assert!(e2.subscriptions().is_empty());
9664 assert!(e2.publications().contains("pub_legacy"));
9665 }
9666
9667 #[test]
9668 fn create_subscription_allowed_inside_transaction() {
9669 let mut e = Engine::new();
9670 e.execute("BEGIN").unwrap();
9671 e.execute("CREATE SUBSCRIPTION s CONNECTION 'h=x' PUBLICATION p")
9672 .unwrap();
9673 e.execute("COMMIT").unwrap();
9674 assert!(e.subscriptions().contains("s"));
9675 }
9676
9677 #[test]
9678 #[test]
9681 fn analyze_populates_histogram_bounds() {
9682 let mut e = Engine::new();
9683 e.execute("CREATE TABLE t (id INT NOT NULL, name TEXT)").unwrap();
9684 for i in 0..50 {
9685 e.execute(&alloc::format!(
9686 "INSERT INTO t VALUES ({i}, 'name{i}')"
9687 ))
9688 .unwrap();
9689 }
9690 e.execute("ANALYZE t").unwrap();
9691 let stats = e.statistics();
9692 let id_stats = stats.get("t", "id").unwrap();
9693 assert!(id_stats.histogram_bounds.len() >= 2);
9694 assert_eq!(id_stats.histogram_bounds.first().unwrap(), "0");
9695 assert_eq!(id_stats.histogram_bounds.last().unwrap(), "49");
9696 assert!((id_stats.null_frac - 0.0).abs() < 1e-6);
9697 assert_eq!(id_stats.n_distinct, 50);
9698 }
9699
9700 #[test]
9701 fn reanalyze_overwrites_prior_stats() {
9702 let mut e = Engine::new();
9703 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9704 for i in 0..10 {
9705 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9706 }
9707 e.execute("ANALYZE t").unwrap();
9708 let n1 = e.statistics().get("t", "id").unwrap().n_distinct;
9709 assert_eq!(n1, 10);
9710 for i in 10..30 {
9711 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9712 }
9713 e.execute("ANALYZE t").unwrap();
9714 let n2 = e.statistics().get("t", "id").unwrap().n_distinct;
9715 assert_eq!(n2, 30);
9716 }
9717
9718 #[test]
9719 fn analyze_unknown_table_errors() {
9720 let mut e = Engine::new();
9721 let err = e.execute("ANALYZE nonexistent").unwrap_err();
9722 assert!(matches!(err, EngineError::Storage(StorageError::TableNotFound { .. })));
9723 }
9724
9725 #[test]
9726 fn bare_analyze_covers_all_user_tables() {
9727 let mut e = Engine::new();
9728 e.execute("CREATE TABLE t1 (id INT NOT NULL)").unwrap();
9729 e.execute("CREATE TABLE t2 (name TEXT NOT NULL)").unwrap();
9730 e.execute("INSERT INTO t1 VALUES (1)").unwrap();
9731 e.execute("INSERT INTO t2 VALUES ('alice')").unwrap();
9732 let r = e.execute("ANALYZE").unwrap();
9733 match r {
9734 QueryResult::CommandOk { affected, modified_catalog } => {
9735 assert_eq!(affected, 2);
9736 assert!(modified_catalog);
9737 }
9738 other => panic!("expected CommandOk, got {other:?}"),
9739 }
9740 assert!(e.statistics().get("t1", "id").is_some());
9741 assert!(e.statistics().get("t2", "name").is_some());
9742 }
9743
9744 #[test]
9745 fn select_from_spg_statistic_returns_rows_per_column() {
9746 let mut e = Engine::new();
9747 e.execute("CREATE TABLE t (id INT NOT NULL, label TEXT)")
9748 .unwrap();
9749 e.execute("INSERT INTO t VALUES (1, 'a')").unwrap();
9750 e.execute("INSERT INTO t VALUES (2, 'b')").unwrap();
9751 e.execute("ANALYZE t").unwrap();
9752 let r = e.execute_readonly("SELECT * FROM spg_statistic").unwrap();
9753 let QueryResult::Rows { rows, columns } = r else {
9754 panic!()
9755 };
9756 assert_eq!(columns.len(), 6);
9758 assert_eq!(columns[0].name, "table_name");
9759 assert_eq!(columns[4].name, "histogram_bounds");
9760 assert_eq!(columns[5].name, "cold_row_count");
9761 assert_eq!(rows.len(), 2, "one row per column of t");
9762 match (&rows[0].values[0], &rows[0].values[1]) {
9764 (Value::Text(t), Value::Text(c)) => {
9765 assert_eq!(t, "t");
9766 assert_eq!(c, "id");
9768 }
9769 _ => panic!(),
9770 }
9771 }
9772
9773 #[test]
9774 fn analyze_skips_vector_columns() {
9775 let mut e = Engine::new();
9778 e.execute("CREATE TABLE t (id INT NOT NULL, v VECTOR(3) NOT NULL)")
9779 .unwrap();
9780 e.execute("INSERT INTO t VALUES (1, [1, 2, 3])").unwrap();
9781 e.execute("ANALYZE t").unwrap();
9782 assert!(e.statistics().get("t", "id").is_some());
9783 assert!(e.statistics().get("t", "v").is_none());
9784 }
9785
9786 #[test]
9787 fn statistics_persist_across_envelope_v5_round_trip() {
9788 let mut e = Engine::new();
9789 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9790 for i in 0..20 {
9791 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9792 }
9793 e.execute("ANALYZE").unwrap();
9794 let snap = e.snapshot();
9795 let e2 = Engine::restore_envelope(&snap).unwrap();
9796 let s = e2.statistics().get("t", "id").unwrap();
9797 assert_eq!(s.n_distinct, 20);
9798 }
9799
9800 #[test]
9803 fn auto_analyze_threshold_fires_after_10pct_of_min_rows_on_small_table() {
9804 let mut e = Engine::new();
9808 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9809 for i in 0..9 {
9810 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9811 }
9812 assert!(e.tables_needing_analyze().is_empty(), "9 < threshold");
9813 e.execute("INSERT INTO t VALUES (9)").unwrap();
9814 let needs = e.tables_needing_analyze();
9815 assert_eq!(needs, alloc::vec!["t".to_string()]);
9816 }
9817
9818 #[test]
9819 fn auto_analyze_threshold_uses_10pct_of_row_count_for_large_tables() {
9820 let mut e = Engine::new();
9826 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9827 for i in 0..1000 {
9828 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9829 }
9830 e.execute("ANALYZE t").unwrap();
9831 assert!(e.tables_needing_analyze().is_empty(), "fresh ANALYZE");
9832 for i in 1000..1050 {
9833 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9834 }
9835 assert!(
9836 e.tables_needing_analyze().is_empty(),
9837 "50 inserts < threshold of ~105"
9838 );
9839 for i in 1050..1200 {
9840 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9841 }
9842 assert_eq!(
9843 e.tables_needing_analyze(),
9844 alloc::vec!["t".to_string()],
9845 "200 inserts > 0.1 × 1200 threshold"
9846 );
9847 }
9848
9849 #[test]
9850 fn auto_analyze_threshold_resets_after_analyze() {
9851 let mut e = Engine::new();
9852 e.execute("CREATE TABLE t (id INT NOT NULL)").unwrap();
9853 for i in 0..200 {
9854 e.execute(&alloc::format!("INSERT INTO t VALUES ({i})")).unwrap();
9855 }
9856 assert!(!e.tables_needing_analyze().is_empty());
9857 e.execute("ANALYZE").unwrap();
9858 assert!(
9859 e.tables_needing_analyze().is_empty(),
9860 "ANALYZE must reset the counter"
9861 );
9862 }
9863
9864 #[test]
9865 fn auto_analyze_threshold_tracks_updates_and_deletes() {
9866 let mut e = Engine::new();
9867 e.execute("CREATE TABLE t (id INT NOT NULL, label TEXT)").unwrap();
9868 for i in 0..50 {
9869 e.execute(&alloc::format!("INSERT INTO t VALUES ({i}, 'x')"))
9870 .unwrap();
9871 }
9872 e.execute("ANALYZE t").unwrap();
9873 e.execute("UPDATE t SET label = 'y' WHERE id < 20").unwrap();
9876 e.execute("DELETE FROM t WHERE id >= 45").unwrap();
9877 assert_eq!(
9878 e.tables_needing_analyze(),
9879 alloc::vec!["t".to_string()]
9880 );
9881 }
9882
9883 #[test]
9884 fn v4_envelope_loads_with_empty_statistics() {
9885 let mut e = Engine::new();
9889 e.create_user("alice", "secret", crate::users::Role::ReadOnly, [0u8; 16])
9890 .unwrap();
9891 let catalog = e.catalog.serialize();
9892 let users = crate::users::serialize_users(&e.users);
9893 let pubs = e.publications.serialize();
9894 let subs = e.subscriptions.serialize();
9895 let mut buf = Vec::new();
9896 buf.extend_from_slice(b"SPGENV01");
9897 buf.push(4u8);
9898 buf.extend_from_slice(&u32::try_from(catalog.len()).unwrap().to_le_bytes());
9899 buf.extend_from_slice(&catalog);
9900 buf.extend_from_slice(&u32::try_from(users.len()).unwrap().to_le_bytes());
9901 buf.extend_from_slice(&users);
9902 buf.extend_from_slice(&u32::try_from(pubs.len()).unwrap().to_le_bytes());
9903 buf.extend_from_slice(&pubs);
9904 buf.extend_from_slice(&u32::try_from(subs.len()).unwrap().to_le_bytes());
9905 buf.extend_from_slice(&subs);
9906 let crc = spg_crypto::crc32::crc32(&buf);
9907 buf.extend_from_slice(&crc.to_le_bytes());
9908 let e2 = Engine::restore_envelope(&buf).expect("v4 envelope restores");
9909 assert!(e2.statistics().is_empty());
9910 }
9911
9912 #[test]
9913 fn v1_v2_envelope_loads_with_empty_publications() {
9914 let mut e = Engine::new();
9921 e.create_user(
9924 "alice",
9925 "secret",
9926 crate::users::Role::ReadOnly,
9927 [0u8; 16],
9928 )
9929 .unwrap();
9930
9931 let catalog = e.catalog.serialize();
9933 let users = crate::users::serialize_users(&e.users);
9934 let mut buf = Vec::new();
9935 buf.extend_from_slice(b"SPGENV01");
9936 buf.push(2u8); buf.extend_from_slice(
9938 &u32::try_from(catalog.len()).unwrap().to_le_bytes(),
9939 );
9940 buf.extend_from_slice(&catalog);
9941 buf.extend_from_slice(
9942 &u32::try_from(users.len()).unwrap().to_le_bytes(),
9943 );
9944 buf.extend_from_slice(&users);
9945 let crc = spg_crypto::crc32::crc32(&buf);
9946 buf.extend_from_slice(&crc.to_le_bytes());
9947
9948 let e2 = Engine::restore_envelope(&buf).expect("v2 envelope restores");
9949 assert!(e2.publications().is_empty());
9950 }
9951}