#![no_std]
extern crate alloc;
#[cfg(not(feature = "perf-counters"))]
#[macro_export]
macro_rules! bump_counter {
($c:path) => {{
let _ = &$c;
}};
($c:path, $n:expr) => {{
let _ = &$c;
let _ = &$n;
}};
}
#[cfg(feature = "perf-counters")]
#[macro_export]
macro_rules! bump_counter {
($c:path) => {{
$c.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}};
($c:path, $n:expr) => {{
$c.fetch_add($n, core::sync::atomic::Ordering::Relaxed);
}};
}
mod acl;
pub mod aggregate;
pub(crate) mod amcheck;
mod bytebudget;
mod cancel;
mod clock;
mod collate;
mod collate_derive;
mod constraints;
mod conversions;
pub mod copy;
mod cursor;
mod ddl;
pub mod describe;
mod dml;
mod envelope;
pub mod eval;
mod execute;
mod explain;
mod expr_analysis;
pub(crate) mod extsort;
pub mod fts;
mod guc_catalog;
mod index_access;
mod join;
mod join_using;
mod joinfold;
pub mod json;
pub mod largeobject;
mod limit_expr;
pub mod locks;
mod maintenance;
pub mod memoize;
mod notify;
mod numeric;
mod orderby;
mod partition;
pub(crate) mod partition_walks;
pub mod plan_cache;
mod plpgsql;
pub mod publications;
pub mod query_stats;
mod readonly;
pub mod reorder;
mod rls;
mod rules;
pub mod scalarsq_streaming;
mod select;
pub mod selectivity;
mod sequence;
mod session;
mod show;
mod spg_admin;
pub mod statistics;
pub mod subquery;
pub mod subscriptions;
mod substitute;
mod system_catalog;
mod table_access;
pub mod tempstore;
pub mod testkit;
mod transaction;
pub(crate) use transaction::{TxStmtClass, classify_stmt_for_tx};
pub mod triggers;
pub mod users;
mod window;
pub use crate::users::{Role, ScramSecrets, UserError, UserStore};
pub use cancel::{CancelToken, MonotonicNowFn};
pub use execute::{RowCells, StreamItem};
use bytebudget::*;
pub(crate) use clock::{rewrite_clock_calls, value_to_literal};
use constraints::*;
pub use constraints::{UNIQ_FOLD_CHOSEN, UNIQ_PROBE_CALLS, UNIQ_PROBE_LOCATORS};
use conversions::*;
pub use conversions::{
format_bigint_2d_text_pub, format_bit_string, format_circle, format_hstore_text, format_inet,
format_int_2d_text_pub, format_line, format_lseg, format_macaddr, format_macaddr8,
format_multirange, format_path, format_pg_box, format_pg_lsn, format_point, format_polygon,
format_range_text, format_text_2d_text_pub,
};
pub(crate) use ddl::{
canonicalize_set_value, enforce_enum_label, eval_runtime_default_free,
resolve_column_default_free,
};
pub(crate) use envelope::{EnvelopeParse, build_envelope, split_envelope};
use expr_analysis::*;
use index_access::*;
pub use join::{ANTI_JOIN_FAST_PATH_FIRED, ANTI_JOIN_FAST_PATH_TRIED};
pub(crate) use orderby::{
OrderKey, apply_offset_and_limit, apply_offset_and_limit_tagged, build_order_keys,
canonical_value_repr, cmp_multi_key, expand_group_by_all, order_by_value_cmp,
order_by_value_cmp_in, render_histogram_bounds, resolve_order_by_position, sort_by_keys,
sort_values_for_histogram, topk_trim, value_cmp, value_to_f64,
};
pub use select::{DISTINCT_DUP_DROPPED, PROJ_DIRECT_FIRE, PROJ_ROW_BUILT, SCAN_PATH_ENTERED};
pub(crate) use select::{build_projection, infer_column_types, value_to_order_key};
pub use sequence::MUTATING_CALL_NEEDLES;
pub(crate) use show::render_create_table;
pub use subquery::{
BATCHED_SCALAR_FALL_THROUGH_COUNT, BATCHED_SCALAR_KEYED_FIRE_COUNT,
BATCHED_SCALAR_KEYED_PROBE_COUNT, EXISTS_BATCH_FALL_THROUGH_COUNT, EXISTS_BATCH_FIRE_COUNT,
EXISTS_PULLUP_BAIL_INNER_FROM, EXISTS_PULLUP_BAIL_INNER_SHAPE,
EXISTS_PULLUP_BAIL_MULTICOL_DISABLED, EXISTS_PULLUP_BAIL_NO_CORR, EXISTS_PULLUP_BAIL_NO_WHERE,
EXISTS_PULLUP_BAIL_RESIDUAL_NOT_INNER, EXISTS_PULLUP_BAIL_UNIQUE_KEY_MISSING,
EXISTS_PULLUP_CANDIDATE_COUNT, EXISTS_PULLUP_FIRE_COUNT, EXISTS_PULLUP_MULTICOL_DISABLE,
PULLUP_LIMIT1_FIRE_COUNT, SCALARSQ_PK_PROBE_FIRED, ScalarPkProbeFastPath,
expr_tree_has_subquery,
};
pub(crate) use subquery::{build_in_list_set, collect_scalar_subqueries, expr_has_subquery};
pub use substitute::substitute_placeholders;
use substitute::*;
use system_catalog::*;
use window::*;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
pub use spg_sql::ast::{SelectStatement, Statement as ParsedStatement};
pub use spg_storage::RowChange;
pub use spg_storage::row_header::{RowHeader, XMAX_ALIVE, XMIN_FROZEN};
pub use spg_storage::snapshot::{
AllCommitted, InProgressSet, Snapshot, XactStatus, XactStatusOracle,
};
impl XactStatusOracle for Engine {
fn status(&self, version: u64) -> XactStatus {
self.xact_status(version)
}
}
use spg_sql::parser::ParseError;
pub use spg_sql::silent_for_update_count;
use spg_storage::{Catalog, ColumnSchema, Row, StorageError};
use crate::eval::EvalError;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum QueryResult {
CommandOk {
affected: usize,
modified_catalog: bool,
},
Rows {
columns: Vec<ColumnSchema>,
rows: Vec<Row<'static>>,
},
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum EngineError {
Parse(ParseError),
Storage(StorageError),
Eval(EvalError),
Unsupported(String),
TransactionAlreadyOpen,
NoActiveTransaction,
InFailedTransaction,
LockWouldBlock,
LockDeadlock,
CardinalityViolation,
SerializationFailure(String),
WriteRequired,
RowLimitExceeded(usize),
QueryBytesExceeded(usize),
Cancelled,
UnknownThreadId(u32),
ConnectionKilled,
Internal(String),
}
impl fmt::Display for EngineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Parse(e) => write!(f, "parse: {e}"),
Self::Storage(e) => write!(f, "storage: {e}"),
Self::Eval(e) => write!(f, "eval: {e}"),
Self::Unsupported(s) => write!(f, "unsupported: {s}"),
Self::TransactionAlreadyOpen => f.write_str("a transaction is already open"),
Self::NoActiveTransaction => f.write_str("no active transaction"),
Self::LockWouldBlock => f.write_str("row is locked by another transaction"),
Self::LockDeadlock => f.write_str("deadlock detected"),
Self::InFailedTransaction => f.write_str(
"current transaction is aborted, commands ignored until end of transaction block",
),
Self::CardinalityViolation => {
f.write_str("more than one row returned by a subquery used as an expression")
}
Self::SerializationFailure(detail) => {
if detail.starts_with("could not serialize access") {
f.write_str(detail)
} else {
write!(
f,
"could not serialize access due to concurrent update: {detail}"
)
}
}
Self::WriteRequired => {
f.write_str("statement requires a write lock (use execute, not execute_readonly)")
}
Self::RowLimitExceeded(n) => {
write!(f, "query exceeded max_query_rows={n}")
}
Self::QueryBytesExceeded(n) => {
write!(
f,
"query materialisation exceeded max_query_bytes={n} (set SPG_MAX_QUERY_BYTES to raise, 0 to disable)"
)
}
Self::Cancelled => f.write_str("query cancelled (timeout or client request)"),
Self::UnknownThreadId(id) => write!(f, "Unknown thread id: {id}"),
Self::ConnectionKilled => f.write_str("Connection was killed"),
Self::Internal(s) => write!(f, "internal error: {s}"),
}
}
}
impl From<ParseError> for EngineError {
fn from(e: ParseError) -> Self {
Self::Parse(e)
}
}
impl From<StorageError> for EngineError {
fn from(e: StorageError) -> Self {
Self::Storage(e)
}
}
impl From<EvalError> for EngineError {
fn from(e: EvalError) -> Self {
Self::Eval(e)
}
}
pub type ClockFn = fn() -> i64;
pub type BackendCountFn = fn() -> u32;
pub type BackendPidFn = fn() -> u32;
pub type WalLsnFn = fn() -> u64;
pub type BackendSignalFn = fn(pid: u32, terminate: bool) -> bool;
pub use tempstore::{SpillStats, TempRun, TempRunFactory, TempStoreError};
#[must_use]
pub fn pg_guc_boot_value(name: &str) -> Option<&'static str> {
crate::guc_catalog::guc_boot_value(name)
}
pub type TzOffsetFn = fn(&str, i64) -> Option<i64>;
pub type TzLocalizeFn = fn(&str, i64) -> Option<i64>;
pub type TzCanonFn = fn(&str) -> Option<alloc::string::String>;
pub type TzAbbrevFn = fn(&str, i64) -> Option<alloc::string::String>;
pub type TzAllFn =
fn(i64) -> alloc::vec::Vec<(alloc::string::String, alloc::string::String, i64, bool)>;
#[derive(Debug, Clone)]
pub enum SessionTz {
Utc,
Fixed(i64),
Named(alloc::string::String, TzOffsetFn, TzAbbrevFn),
}
impl SessionTz {
#[must_use]
pub fn is_utc(&self) -> bool {
matches!(self, Self::Utc) || matches!(self, Self::Fixed(0))
}
#[must_use]
pub fn offset_at(&self, utc_micros: i64) -> i64 {
match self {
Self::Utc => 0,
Self::Fixed(off) => *off,
Self::Named(zone, f, _) => f(zone, utc_micros).unwrap_or(0),
}
}
#[must_use]
pub fn abbrev_at(&self, utc_micros: i64) -> Option<alloc::string::String> {
match self {
Self::Named(zone, _, f) => f(zone, utc_micros),
_ => None,
}
}
}
pub type SaltFn = fn() -> [u8; 16];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TxId(pub u64);
pub const IMPLICIT_TX: TxId = TxId(0);
pub const COMPACTION_TARGET_DEFAULT_BYTES: u64 = 4 * 1024 * 1024;
#[derive(Debug, Default, Clone)]
struct TxState {
catalog: Catalog,
users: Option<crate::users::UserStore>,
savepoints: Vec<(String, Catalog, Option<crate::users::UserStore>)>,
cached_snapshot: Option<spg_storage::snapshot::Snapshot>,
touched_tables: alloc::collections::BTreeSet<String>,
read_tables: alloc::collections::BTreeSet<String>,
serializable: bool,
begin_commit_seq: u64,
shadow_dirty: bool,
aborted: bool,
constraints_deferred: Option<bool>,
constraints_deferred_by_name: BTreeMap<String, bool>,
rebase_poisoned: bool,
stmts_run: u32,
rebased_at_epoch: u64,
update_pairs: alloc::collections::BTreeMap<
String,
Vec<(
spg_storage::row_header::RowId,
spg_storage::row_header::RowId,
)>,
>,
}
#[derive(Debug, Clone)]
pub struct CatalogSnapshot {
catalog: Catalog,
statistics: statistics::Statistics,
clock: Option<ClockFn>,
max_query_rows: Option<usize>,
}
#[derive(Debug, Default)]
pub(crate) struct SessionBag {
pub(crate) session_params: BTreeMap<String, String>,
pub(crate) backslash_escapes: bool,
pub(crate) mysql_strict: bool,
pub(crate) prepared_statements: BTreeMap<String, PreparedSqlStatement>,
pub(crate) seq_currvals: BTreeMap<String, i64>,
pub(crate) last_sequence_used: Option<String>,
pub(crate) isolation_level: spg_sql::ast::IsolationLevel,
pub(crate) lo_descriptors: BTreeMap<i32, LargeObjectDescriptor>,
pub(crate) lo_next_fd: i32,
pub(crate) cursors: BTreeMap<String, cursor::OpenCursor>,
pub(crate) last_insert_id: i64,
pub(crate) row_count: i64,
pub(crate) user_vars: BTreeMap<String, spg_storage::Value<'static>>,
pub(crate) temp_tables: alloc::collections::BTreeSet<String>,
pub(crate) temp_sequences: alloc::collections::BTreeSet<String>,
pub(crate) temp_views: alloc::collections::BTreeSet<String>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct LargeObjectDescriptor {
pub(crate) oid: u32,
pub(crate) pos: u64,
pub(crate) writable: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct PreparedSqlStatement {
pub(crate) body: spg_sql::ast::Statement,
pub(crate) param_types: alloc::vec::Vec<String>,
pub(crate) source: String,
}
#[derive(Debug, Clone)]
pub struct EngineSnapshot {
catalog: Catalog,
users: UserStore,
publications: publications::Publications,
subscriptions: subscriptions::Subscriptions,
statistics: statistics::Statistics,
}
impl EngineSnapshot {
pub fn serialize(&self) -> Vec<u8> {
if self.users.is_empty()
&& self.publications.is_empty()
&& self.subscriptions.is_empty()
&& self.statistics.is_empty()
{
self.catalog.serialize()
} else {
build_envelope(
&self.catalog.serialize(),
&users::serialize_users(&self.users),
&self.publications.serialize(),
&self.subscriptions.serialize(),
&self.statistics.serialize(),
)
}
}
}
pub trait ParallelRunner: Send + Sync {
fn run_shards(
&self,
n: usize,
f: &(dyn Fn(usize) -> alloc::boxed::Box<dyn core::any::Any + Send> + Sync),
) -> alloc::vec::Vec<alloc::boxed::Box<dyn core::any::Any + Send>>;
}
#[derive(Clone, Default)]
pub struct ParallelRunnerSlot(pub(crate) Option<alloc::sync::Arc<dyn ParallelRunner>>);
impl core::fmt::Debug for ParallelRunnerSlot {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(if self.0.is_some() {
"ParallelRunner(<injected>)"
} else {
"ParallelRunner(none)"
})
}
}
pub(crate) const PARALLEL_MIN_ROWS: usize = 100_000;
pub static MATVIEW_FANOUT_BUFFERED: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static MATVIEW_DELTA_APPLIED: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static MATVIEW_DELTA_BAILED: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static PARALLEL_AGG_FIRED: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Default)]
pub struct Engine {
pub(crate) parallel_runner: ParallelRunnerSlot,
catalog: Catalog,
tx_catalogs: BTreeMap<TxId, TxState>,
table_last_commit: BTreeMap<String, u64>,
commit_seq: u64,
current_tx: Option<TxId>,
next_tx_id: u64,
active_writer_versions: BTreeSet<u64>,
aborted_versions: BTreeSet<u64>,
locks: crate::locks::LockTable,
mvcc_inplace: bool,
autovacuum: bool,
autovacuum_inline: bool,
pub(crate) lock_skip_rows: Option<(String, alloc::collections::BTreeSet<usize>)>,
tx_writer_versions: BTreeMap<TxId, u64>,
stmt_writer_version: Option<u64>,
backslash_escapes: bool,
mysql_strict: bool,
pub(crate) lo_descriptors: BTreeMap<i32, LargeObjectDescriptor>,
pub(crate) lo_next_fd: i32,
last_sequence_used: Option<String>,
seq_currvals: alloc::collections::BTreeMap<String, i64>,
prepared_statements: alloc::collections::BTreeMap<String, PreparedSqlStatement>,
current_session: u32,
sessions: BTreeMap<u32, SessionBag>,
advisory_locks: BTreeMap<i64, (u32, u32)>,
clock: Option<ClockFn>,
salt_fn: Option<SaltFn>,
max_query_rows: Option<usize>,
pub(crate) max_query_bytes: Option<usize>,
pub(crate) temp_run_factory: Option<crate::TempRunFactory>,
pub(crate) users: UserStore,
publications: publications::Publications,
subscriptions: subscriptions::Subscriptions,
statistics: statistics::Statistics,
plan_cache: plan_cache::PlanCache,
query_stats: query_stats::QueryStats,
activity_provider: Option<ActivityProvider>,
audit_chain_provider: Option<AuditChainProvider>,
audit_verifier: Option<AuditVerifier>,
slow_query_threshold_us: Option<u64>,
slow_query_logger: Option<SlowQueryLogger>,
pub(crate) session_params: BTreeMap<String, String>,
pub(crate) cursors: BTreeMap<String, cursor::OpenCursor>,
pub(crate) last_insert_id: core::sync::atomic::AtomicI64,
pub(crate) row_count: i64,
pub(crate) user_vars: BTreeMap<String, spg_storage::Value<'static>>,
pub(crate) temp_tables: BTreeSet<String>,
pub(crate) temp_sequences: BTreeSet<String>,
pub(crate) temp_views: BTreeSet<String>,
pub(crate) listen_channels: BTreeSet<String>,
pub(crate) tx_pending_notifies: Vec<(String, String)>,
pub(crate) delivered_notifies: Vec<(String, String)>,
pending_notices: Vec<Notice>,
pub(crate) spill_stats: crate::tempstore::SpillStats,
pub(crate) xact_commit: core::sync::atomic::AtomicU64,
pub(crate) xact_rollback: core::sync::atomic::AtomicU64,
pub(crate) backend_count_fn: Option<BackendCountFn>,
pub(crate) backend_pid_fn: Option<BackendPidFn>,
pub(crate) wal_lsn_fn: Option<WalLsnFn>,
pub(crate) backend_signal_fn: Option<BackendSignalFn>,
pub(crate) tz_offset_fn: Option<TzOffsetFn>,
pub(crate) tz_localize_fn: Option<TzLocalizeFn>,
pub(crate) tz_canon_fn: Option<TzCanonFn>,
pub(crate) tz_abbrev_fn: Option<TzAbbrevFn>,
pub(crate) tz_all_fn: Option<TzAllFn>,
pub(crate) stat_tup_inserted: u64,
pub(crate) stat_tup_updated: u64,
pub(crate) stat_tup_deleted: u64,
pub(crate) table_write_stats: alloc::collections::BTreeMap<String, (u64, u64, u64)>,
pub(crate) commit_epoch: u64,
pub(crate) local_guc_saves: Vec<(String, Option<String>)>,
pub(crate) render_style: crate::eval::RenderStyle,
pub(crate) savepoint_guc_marks: Vec<(String, usize)>,
trigger_recursion_depth: u32,
rule_rewrite_active: bool,
foreign_key_checks: bool,
meta_views_materialised: bool,
pending_foreign_keys: Vec<(alloc::string::String, spg_sql::ast::ForeignKeyConstraint)>,
env_cfg: testkit::EnvConfig,
#[cfg(feature = "injection-points")]
injection_store: alloc::sync::Arc<crate::testkit::injection::InjectionStore>,
redo_capture: bool,
last_redo: Vec<RowChange>,
table_change_seq: alloc::collections::BTreeMap<String, u64>,
matview_refresh_watermark: alloc::collections::BTreeMap<String, Vec<(String, u64)>>,
matview_maintainable: alloc::collections::BTreeMap<String, String>,
matview_delta_buf: alloc::collections::BTreeMap<String, Vec<RowChange>>,
matview_delta_overflow: alloc::collections::BTreeSet<String>,
matview_row_map:
alloc::collections::BTreeMap<String, (usize, alloc::collections::BTreeMap<u64, usize>)>,
pub(crate) current_isolation_level: spg_sql::ast::IsolationLevel,
}
const MAX_TRIGGER_RECURSION: u32 = 16;
pub type SlowQueryLogger = fn(&str, u64);
#[derive(Debug, Clone)]
pub struct ActivityRow {
pub pid: u32,
pub user: String,
pub client_addr: String,
pub client_port: i32,
pub database: String,
pub started_at_us: i64,
pub current_sql: String,
pub wait_event_type: String,
pub wait_event: String,
pub elapsed_us: i64,
pub in_transaction: bool,
pub application_name: String,
pub backend_type: String,
}
impl ActivityRow {
#[must_use]
pub fn background(pid: u32, backend_type: &str) -> Self {
Self {
pid,
user: String::new(),
client_addr: String::new(),
client_port: -1,
database: String::new(),
started_at_us: 0,
current_sql: String::new(),
wait_event_type: String::new(),
wait_event: String::new(),
elapsed_us: 0,
in_transaction: false,
application_name: String::new(),
backend_type: backend_type.into(),
}
}
}
pub type ActivityProvider = fn() -> Vec<ActivityRow>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoticeSeverity {
Notice,
Warning,
Info,
}
impl NoticeSeverity {
#[must_use]
pub const fn as_pg_str(self) -> &'static str {
match self {
Self::Notice => "NOTICE",
Self::Warning => "WARNING",
Self::Info => "INFO",
}
}
}
#[derive(Debug, Clone)]
pub struct Notice {
pub severity: NoticeSeverity,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct AuditRow {
pub seq: i64,
pub ts_ms: i64,
pub prev_hash_hex: String,
pub entry_hash_hex: String,
pub sql: String,
}
pub type AuditChainProvider = fn() -> Vec<AuditRow>;
pub type AuditVerifier = fn() -> (i64, i64);
impl Engine {
pub fn new() -> Self {
Self {
catalog: Catalog::new(),
parallel_runner: ParallelRunnerSlot::default(),
tx_catalogs: BTreeMap::new(),
table_last_commit: BTreeMap::new(),
commit_seq: 0,
current_tx: None,
backslash_escapes: false,
mysql_strict: true,
lo_descriptors: BTreeMap::new(),
lo_next_fd: 0,
prepared_statements: alloc::collections::BTreeMap::new(),
current_session: 0,
sessions: BTreeMap::new(),
advisory_locks: BTreeMap::new(),
last_sequence_used: None,
seq_currvals: alloc::collections::BTreeMap::new(),
next_tx_id: 1,
active_writer_versions: BTreeSet::new(),
aborted_versions: BTreeSet::new(),
locks: crate::locks::LockTable::new(),
mvcc_inplace: !cfg!(feature = "mvcc-inplace-off"),
autovacuum: true,
autovacuum_inline: true,
lock_skip_rows: None,
tx_writer_versions: BTreeMap::new(),
stmt_writer_version: None,
clock: None,
salt_fn: None,
max_query_rows: None,
max_query_bytes: None,
temp_run_factory: None,
users: UserStore::new(),
publications: publications::Publications::new(),
subscriptions: subscriptions::Subscriptions::new(),
statistics: statistics::Statistics::new(),
plan_cache: plan_cache::PlanCache::new(),
query_stats: query_stats::QueryStats::new(),
activity_provider: None,
audit_chain_provider: None,
audit_verifier: None,
slow_query_threshold_us: None,
slow_query_logger: None,
session_params: BTreeMap::new(),
cursors: BTreeMap::new(),
last_insert_id: core::sync::atomic::AtomicI64::new(0),
row_count: 0,
user_vars: BTreeMap::new(),
temp_tables: BTreeSet::new(),
temp_sequences: BTreeSet::new(),
temp_views: BTreeSet::new(),
listen_channels: BTreeSet::new(),
tx_pending_notifies: Vec::new(),
delivered_notifies: Vec::new(),
pending_notices: Vec::new(),
spill_stats: crate::tempstore::SpillStats::default(),
xact_commit: core::sync::atomic::AtomicU64::new(0),
xact_rollback: core::sync::atomic::AtomicU64::new(0),
backend_count_fn: None,
backend_pid_fn: None,
wal_lsn_fn: None,
backend_signal_fn: None,
tz_offset_fn: None,
tz_localize_fn: None,
tz_canon_fn: None,
tz_abbrev_fn: None,
tz_all_fn: None,
stat_tup_inserted: 0,
table_write_stats: alloc::collections::BTreeMap::new(),
commit_epoch: 0,
stat_tup_updated: 0,
stat_tup_deleted: 0,
local_guc_saves: Vec::new(),
render_style: crate::eval::RenderStyle::default(),
savepoint_guc_marks: Vec::new(),
trigger_recursion_depth: 0,
rule_rewrite_active: false,
foreign_key_checks: true,
meta_views_materialised: false,
pending_foreign_keys: Vec::new(),
env_cfg: testkit::EnvConfig::default(),
#[cfg(feature = "injection-points")]
injection_store: alloc::sync::Arc::new(
crate::testkit::injection::InjectionStore::default(),
),
redo_capture: false,
current_isolation_level: spg_sql::ast::IsolationLevel::ReadCommitted,
last_redo: Vec::new(),
table_change_seq: alloc::collections::BTreeMap::new(),
matview_refresh_watermark: alloc::collections::BTreeMap::new(),
matview_maintainable: alloc::collections::BTreeMap::new(),
matview_delta_buf: alloc::collections::BTreeMap::new(),
matview_delta_overflow: alloc::collections::BTreeSet::new(),
matview_row_map: alloc::collections::BTreeMap::new(),
}
}
#[must_use]
pub fn clone_snapshot(&self) -> CatalogSnapshot {
CatalogSnapshot {
catalog: self.active_catalog().clone(),
statistics: self.statistics.clone(),
clock: self.clock,
max_query_rows: self.max_query_rows,
}
}
#[must_use]
pub fn role_exists(&self, name: &str) -> bool {
if name == self.session_user() {
return true;
}
self.effective_users().contains(name)
|| name.eq_ignore_ascii_case("admin")
|| name.eq_ignore_ascii_case("postgres")
}
#[must_use]
pub fn role_name_for_oid(&self, oid: i64) -> Option<String> {
if oid == 10 {
return Some(alloc::string::String::from("postgres"));
}
let idx = usize::try_from(oid - 11).ok()?;
self.users
.iter()
.nth(idx)
.map(|(n, _)| alloc::string::String::from(n))
}
#[must_use]
pub fn current_snapshot(&self) -> spg_storage::snapshot::Snapshot {
let locked_out = self.lock_skip_rows.as_ref().and_then(|(t, set)| {
self.active_catalog()
.get(t)
.map(|tbl| (tbl.rel_id(), set.clone()))
});
let mut snap = self.current_snapshot_inner();
snap.locked_out = locked_out;
snap
}
fn current_snapshot_inner(&self) -> spg_storage::snapshot::Snapshot {
if let Some(tx_id) = self.current_tx
&& let Some(state) = self.tx_catalogs.get(&tx_id)
&& let Some(s) = state.cached_snapshot.as_ref()
{
return s.clone();
}
let reader_tx_id = self
.current_tx
.and_then(|t| self.tx_writer_versions.get(&t).copied())
.unwrap_or(0);
let version = spg_storage::row_header::current_version();
if self.active_writer_versions.is_empty() {
return spg_storage::snapshot::Snapshot::new(
version,
spg_storage::snapshot::InProgressSet::empty(),
version,
reader_tx_id,
);
}
let sorted: alloc::vec::Vec<u64> = self.active_writer_versions.iter().copied().collect();
let oldest = *sorted.first().unwrap_or(&version);
spg_storage::snapshot::Snapshot::new(
version,
spg_storage::snapshot::InProgressSet::from_sorted(sorted),
oldest,
reader_tx_id,
)
}
pub fn begin_writer_version(&mut self) -> u64 {
let v = spg_storage::row_header::next_version();
self.active_writer_versions.insert(v);
v
}
pub fn commit_writer_version(&mut self, v: u64) {
self.active_writer_versions.remove(&v);
}
pub fn abort_writer_version(&mut self, v: u64) {
self.active_writer_versions.remove(&v);
self.aborted_versions.insert(v);
}
#[must_use]
pub fn xact_status(&self, v: u64) -> spg_storage::snapshot::XactStatus {
use spg_storage::snapshot::XactStatus;
if self.active_writer_versions.contains(&v) {
XactStatus::InProgress
} else if self.aborted_versions.contains(&v) {
XactStatus::Aborted
} else {
XactStatus::Committed
}
}
pub fn acquire_row_lock(
&mut self,
rel: spg_storage::row_header::RelId,
row: spg_storage::row_header::RowId,
mode: crate::locks::LockMode,
version: u64,
policy: crate::locks::WaitPolicy,
) -> crate::locks::LockOutcome {
self.locks.acquire(rel, row, mode, version, policy)
}
pub fn release_tx_locks(&mut self, version: u64) {
self.locks.release_all(version);
}
#[must_use]
pub fn locked_row_count(&self) -> usize {
self.locks.locked_row_count()
}
#[must_use]
pub fn mvcc_inplace(&self) -> bool {
self.mvcc_inplace
}
pub fn set_mvcc_inplace(&mut self, on: bool) {
self.mvcc_inplace = on;
}
pub fn set_backend_count_fn(&mut self, f: BackendCountFn) {
self.backend_count_fn = Some(f);
}
pub fn set_wal_lsn_fn(&mut self, f: WalLsnFn) {
self.wal_lsn_fn = Some(f);
}
pub fn set_backend_pid_fn(&mut self, f: BackendPidFn) {
self.backend_pid_fn = Some(f);
}
pub fn set_backend_signal_fn(&mut self, f: BackendSignalFn) {
self.backend_signal_fn = Some(f);
}
pub fn set_temp_run_factory(&mut self, f: crate::TempRunFactory) {
self.temp_run_factory = Some(f);
}
#[must_use]
pub fn can_spill(&self) -> bool {
self.temp_run_factory.is_some()
}
pub(crate) fn open_temp_run(
&self,
) -> Option<Result<alloc::boxed::Box<dyn crate::TempRun>, crate::TempStoreError>> {
self.temp_run_factory.map(|f| f())
}
pub fn set_tz_fns(
&mut self,
offset: TzOffsetFn,
localize: TzLocalizeFn,
canon: TzCanonFn,
abbrev: TzAbbrevFn,
) {
self.tz_offset_fn = Some(offset);
self.tz_localize_fn = Some(localize);
self.tz_canon_fn = Some(canon);
self.tz_abbrev_fn = Some(abbrev);
}
pub fn set_tz_all_fn(&mut self, all: TzAllFn) {
self.tz_all_fn = Some(all);
}
pub(crate) fn tz_all_at(
&self,
utc_micros: i64,
) -> alloc::vec::Vec<(alloc::string::String, alloc::string::String, i64, bool)> {
self.tz_all_fn
.map_or_else(alloc::vec::Vec::new, |f| f(utc_micros))
}
pub fn set_parallel_runner(&mut self, runner: alloc::sync::Arc<dyn ParallelRunner>) {
self.parallel_runner = ParallelRunnerSlot(Some(runner));
}
#[must_use]
pub fn next_writer_version(&self) -> u64 {
spg_storage::row_header::next_version()
}
pub fn writer_version_for_current_stmt(&mut self) -> u64 {
if let Some(tx_id) = self.current_tx
&& let Some(&v) = self.tx_writer_versions.get(&tx_id)
{
return v;
}
if let Some(v) = self.stmt_writer_version {
return v;
}
let v = self.next_writer_version();
self.stmt_writer_version = Some(v);
v
}
pub fn restore(catalog: Catalog) -> Self {
Self {
lock_skip_rows: None,
catalog,
parallel_runner: ParallelRunnerSlot::default(),
tx_catalogs: BTreeMap::new(),
table_last_commit: BTreeMap::new(),
commit_seq: 0,
current_tx: None,
backslash_escapes: false,
mysql_strict: true,
lo_descriptors: BTreeMap::new(),
lo_next_fd: 0,
prepared_statements: alloc::collections::BTreeMap::new(),
current_session: 0,
sessions: BTreeMap::new(),
advisory_locks: BTreeMap::new(),
last_sequence_used: None,
seq_currvals: alloc::collections::BTreeMap::new(),
next_tx_id: 1,
active_writer_versions: BTreeSet::new(),
aborted_versions: BTreeSet::new(),
locks: crate::locks::LockTable::new(),
mvcc_inplace: !cfg!(feature = "mvcc-inplace-off"),
autovacuum: true,
autovacuum_inline: true,
tx_writer_versions: BTreeMap::new(),
stmt_writer_version: None,
clock: None,
salt_fn: None,
max_query_rows: None,
max_query_bytes: None,
temp_run_factory: None,
users: UserStore::new(),
publications: publications::Publications::new(),
subscriptions: subscriptions::Subscriptions::new(),
statistics: statistics::Statistics::new(),
plan_cache: plan_cache::PlanCache::new(),
query_stats: query_stats::QueryStats::new(),
activity_provider: None,
audit_chain_provider: None,
audit_verifier: None,
slow_query_threshold_us: None,
slow_query_logger: None,
session_params: BTreeMap::new(),
cursors: BTreeMap::new(),
last_insert_id: core::sync::atomic::AtomicI64::new(0),
row_count: 0,
user_vars: BTreeMap::new(),
temp_tables: BTreeSet::new(),
temp_sequences: BTreeSet::new(),
temp_views: BTreeSet::new(),
listen_channels: BTreeSet::new(),
tx_pending_notifies: Vec::new(),
delivered_notifies: Vec::new(),
pending_notices: Vec::new(),
spill_stats: crate::tempstore::SpillStats::default(),
xact_commit: core::sync::atomic::AtomicU64::new(0),
xact_rollback: core::sync::atomic::AtomicU64::new(0),
backend_count_fn: None,
backend_pid_fn: None,
wal_lsn_fn: None,
backend_signal_fn: None,
tz_offset_fn: None,
tz_localize_fn: None,
tz_canon_fn: None,
tz_abbrev_fn: None,
tz_all_fn: None,
stat_tup_inserted: 0,
table_write_stats: alloc::collections::BTreeMap::new(),
commit_epoch: 0,
stat_tup_updated: 0,
stat_tup_deleted: 0,
local_guc_saves: Vec::new(),
render_style: crate::eval::RenderStyle::default(),
savepoint_guc_marks: Vec::new(),
trigger_recursion_depth: 0,
rule_rewrite_active: false,
foreign_key_checks: true,
meta_views_materialised: false,
pending_foreign_keys: Vec::new(),
env_cfg: testkit::EnvConfig::default(),
#[cfg(feature = "injection-points")]
injection_store: alloc::sync::Arc::new(
crate::testkit::injection::InjectionStore::default(),
),
redo_capture: false,
current_isolation_level: spg_sql::ast::IsolationLevel::ReadCommitted,
last_redo: Vec::new(),
table_change_seq: alloc::collections::BTreeMap::new(),
matview_refresh_watermark: alloc::collections::BTreeMap::new(),
matview_maintainable: alloc::collections::BTreeMap::new(),
matview_delta_buf: alloc::collections::BTreeMap::new(),
matview_delta_overflow: alloc::collections::BTreeSet::new(),
matview_row_map: alloc::collections::BTreeMap::new(),
}
}
pub fn restore_envelope(buf: &[u8]) -> Result<Self, EngineError> {
match split_envelope(buf) {
EnvelopeParse::Pair {
catalog: catalog_bytes,
users: user_bytes,
publications: pub_bytes,
subscriptions: sub_bytes,
statistics: stats_bytes,
} => {
let mut catalog =
Catalog::deserialize(catalog_bytes).map_err(EngineError::Storage)?;
crate::ddl::rebuild_all_excl_indexes(&mut catalog);
let users = users::deserialize_users(user_bytes)
.map_err(|e| EngineError::Unsupported(alloc::format!("users restore: {e}")))?;
let publications = match pub_bytes {
Some(b) => publications::Publications::deserialize(b).map_err(|e| {
EngineError::Unsupported(alloc::format!("publications restore: {e:?}"))
})?,
None => publications::Publications::new(),
};
let subscriptions = match sub_bytes {
Some(b) => subscriptions::Subscriptions::deserialize(b).map_err(|e| {
EngineError::Unsupported(alloc::format!("subscriptions restore: {e:?}"))
})?,
None => subscriptions::Subscriptions::new(),
};
let statistics = match stats_bytes {
Some(b) => statistics::Statistics::deserialize(b).map_err(|e| {
EngineError::Unsupported(alloc::format!("statistics restore: {e:?}"))
})?,
None => statistics::Statistics::new(),
};
Ok(Self {
lock_skip_rows: None,
catalog,
parallel_runner: ParallelRunnerSlot::default(),
tx_catalogs: BTreeMap::new(),
table_last_commit: BTreeMap::new(),
commit_seq: 0,
current_tx: None,
backslash_escapes: false,
mysql_strict: true,
lo_descriptors: BTreeMap::new(),
lo_next_fd: 0,
prepared_statements: alloc::collections::BTreeMap::new(),
current_session: 0,
sessions: BTreeMap::new(),
advisory_locks: BTreeMap::new(),
last_sequence_used: None,
seq_currvals: alloc::collections::BTreeMap::new(),
next_tx_id: 1,
active_writer_versions: BTreeSet::new(),
aborted_versions: BTreeSet::new(),
locks: crate::locks::LockTable::new(),
mvcc_inplace: !cfg!(feature = "mvcc-inplace-off"),
autovacuum: true,
autovacuum_inline: true,
tx_writer_versions: BTreeMap::new(),
stmt_writer_version: None,
clock: None,
salt_fn: None,
max_query_rows: None,
max_query_bytes: None,
temp_run_factory: None,
users,
publications,
subscriptions,
statistics,
plan_cache: plan_cache::PlanCache::new(),
query_stats: query_stats::QueryStats::new(),
activity_provider: None,
audit_chain_provider: None,
audit_verifier: None,
slow_query_threshold_us: None,
slow_query_logger: None,
session_params: BTreeMap::new(),
cursors: BTreeMap::new(),
last_insert_id: core::sync::atomic::AtomicI64::new(0),
row_count: 0,
user_vars: BTreeMap::new(),
temp_tables: BTreeSet::new(),
temp_sequences: BTreeSet::new(),
temp_views: BTreeSet::new(),
listen_channels: BTreeSet::new(),
tx_pending_notifies: Vec::new(),
delivered_notifies: Vec::new(),
pending_notices: Vec::new(),
spill_stats: crate::tempstore::SpillStats::default(),
xact_commit: core::sync::atomic::AtomicU64::new(0),
xact_rollback: core::sync::atomic::AtomicU64::new(0),
backend_count_fn: None,
backend_pid_fn: None,
wal_lsn_fn: None,
backend_signal_fn: None,
tz_offset_fn: None,
tz_localize_fn: None,
tz_canon_fn: None,
tz_abbrev_fn: None,
tz_all_fn: None,
stat_tup_inserted: 0,
table_write_stats: alloc::collections::BTreeMap::new(),
commit_epoch: 0,
stat_tup_updated: 0,
stat_tup_deleted: 0,
local_guc_saves: Vec::new(),
render_style: crate::eval::RenderStyle::default(),
savepoint_guc_marks: Vec::new(),
trigger_recursion_depth: 0,
rule_rewrite_active: false,
foreign_key_checks: true,
meta_views_materialised: false,
pending_foreign_keys: Vec::new(),
env_cfg: testkit::EnvConfig::default(),
#[cfg(feature = "injection-points")]
injection_store: alloc::sync::Arc::new(
crate::testkit::injection::InjectionStore::default(),
),
redo_capture: false,
current_isolation_level: spg_sql::ast::IsolationLevel::ReadCommitted,
last_redo: Vec::new(),
table_change_seq: alloc::collections::BTreeMap::new(),
matview_refresh_watermark: alloc::collections::BTreeMap::new(),
matview_maintainable: alloc::collections::BTreeMap::new(),
matview_delta_buf: alloc::collections::BTreeMap::new(),
matview_delta_overflow: alloc::collections::BTreeSet::new(),
matview_row_map: alloc::collections::BTreeMap::new(),
})
}
EnvelopeParse::CrcMismatch { expected, computed } => {
Err(EngineError::Storage(StorageError::Corrupt(alloc::format!(
"snapshot envelope CRC32 mismatch (expected={expected:#010x}, computed={computed:#010x})"
))))
}
EnvelopeParse::Bare => {
let mut catalog = Catalog::deserialize(buf).map_err(EngineError::Storage)?;
crate::ddl::rebuild_all_excl_indexes(&mut catalog);
Ok(Self::restore(catalog))
}
}
}
pub const fn users(&self) -> &UserStore {
&self.users
}
pub fn set_current_session(&mut self, id: u32) {
if id == self.current_session {
return;
}
let outgoing = SessionBag {
session_params: core::mem::take(&mut self.session_params),
backslash_escapes: self.backslash_escapes,
mysql_strict: self.mysql_strict,
prepared_statements: core::mem::take(&mut self.prepared_statements),
lo_descriptors: core::mem::take(&mut self.lo_descriptors),
lo_next_fd: self.lo_next_fd,
cursors: core::mem::take(&mut self.cursors),
last_insert_id: self
.last_insert_id
.load(core::sync::atomic::Ordering::Relaxed),
row_count: self.row_count,
user_vars: core::mem::take(&mut self.user_vars),
temp_tables: core::mem::take(&mut self.temp_tables),
temp_sequences: core::mem::take(&mut self.temp_sequences),
temp_views: core::mem::take(&mut self.temp_views),
seq_currvals: core::mem::take(&mut self.seq_currvals),
last_sequence_used: self.last_sequence_used.take(),
isolation_level: self.current_isolation_level,
};
self.sessions.insert(self.current_session, outgoing);
let incoming = self.sessions.remove(&id).unwrap_or_default();
self.session_params = incoming.session_params;
self.backslash_escapes = incoming.backslash_escapes;
self.mysql_strict = incoming.mysql_strict;
self.prepared_statements = incoming.prepared_statements;
self.lo_descriptors = incoming.lo_descriptors;
self.lo_next_fd = incoming.lo_next_fd;
self.cursors = incoming.cursors;
self.last_insert_id.store(
incoming.last_insert_id,
core::sync::atomic::Ordering::Relaxed,
);
self.row_count = incoming.row_count;
self.user_vars = incoming.user_vars;
self.temp_tables = incoming.temp_tables;
self.temp_sequences = incoming.temp_sequences;
self.temp_views = incoming.temp_views;
self.seq_currvals = incoming.seq_currvals;
self.last_sequence_used = incoming.last_sequence_used;
self.current_isolation_level = incoming.isolation_level;
self.current_session = id;
self.refresh_temp_prefix();
self.plan_cache.clear();
}
fn temp_prefix_for(id: u32) -> String {
alloc::format!("__spg_temp_{id}__")
}
pub(crate) fn session_temp_name(&self, logical: &str) -> String {
alloc::format!("{}{logical}", Self::temp_prefix_for(self.current_session))
}
pub(crate) fn refresh_temp_prefix(&mut self) {
let prefix = if self.temp_tables.is_empty()
&& self.temp_sequences.is_empty()
&& self.temp_views.is_empty()
{
None
} else {
Some(Self::temp_prefix_for(self.current_session))
};
self.catalog.set_temp_prefix(prefix.clone());
for shadow in self.tx_catalogs.values_mut() {
shadow.catalog.set_temp_prefix(prefix.clone());
}
}
pub fn end_session(&mut self, id: u32) {
let owned: Vec<String> = if id == self.current_session {
self.temp_tables.iter().cloned().collect()
} else {
self.sessions
.get(&id)
.map(|b| b.temp_tables.iter().cloned().collect())
.unwrap_or_default()
};
let owned_seqs: Vec<String> = if id == self.current_session {
self.temp_sequences.iter().cloned().collect()
} else {
self.sessions
.get(&id)
.map(|b| b.temp_sequences.iter().cloned().collect())
.unwrap_or_default()
};
let owned_views: Vec<String> = if id == self.current_session {
self.temp_views.iter().cloned().collect()
} else {
self.sessions
.get(&id)
.map(|b| b.temp_views.iter().cloned().collect())
.unwrap_or_default()
};
if !owned.is_empty() || !owned_seqs.is_empty() || !owned_views.is_empty() {
let prefix = Self::temp_prefix_for(id);
for logical in owned {
let mangled = alloc::format!("{prefix}{logical}");
self.catalog.drop_table(&mangled);
}
for logical in owned_seqs {
let mangled = alloc::format!("{prefix}{logical}");
self.catalog.drop_sequence(&mangled);
}
for logical in owned_views {
let mangled = alloc::format!("{prefix}{logical}");
self.catalog.drop_view(&mangled);
}
if id == self.current_session {
self.temp_tables.clear();
self.temp_sequences.clear();
self.temp_views.clear();
self.refresh_temp_prefix();
}
}
self.sessions.remove(&id);
self.advisory_locks.retain(|_, (owner, _)| *owner != id);
if id == self.current_session {
self.session_params.clear();
self.prepared_statements.clear();
self.backslash_escapes = false;
self.mysql_strict = true;
self.lo_descriptors.clear();
self.lo_next_fd = 0;
self.cursors.clear();
self.current_session = 0;
}
}
pub fn set_backslash_escapes(&mut self, flag: bool) {
if flag != self.backslash_escapes {
self.backslash_escapes = flag;
self.plan_cache.clear();
}
}
pub(crate) fn advisory_try_lock(&mut self, key: i64) -> bool {
let me = self.current_session;
match self.advisory_locks.get_mut(&key) {
Some((owner, depth)) if *owner == me => {
*depth += 1;
true
}
Some(_) => false,
None => {
self.advisory_locks.insert(key, (me, 1));
true
}
}
}
pub(crate) fn advisory_unlock(&mut self, key: i64) -> bool {
let me = self.current_session;
match self.advisory_locks.get_mut(&key) {
Some((owner, depth)) if *owner == me => {
*depth -= 1;
if *depth == 0 {
self.advisory_locks.remove(&key);
}
true
}
_ => false,
}
}
pub(crate) fn advisory_unlock_all(&mut self) {
let me = self.current_session;
self.advisory_locks.retain(|_, (owner, _)| *owner != me);
}
pub(crate) fn user_var(&self, name: &str) -> Option<&spg_storage::Value<'static>> {
self.user_vars.get(name)
}
pub(crate) const fn current_session_id(&self) -> u32 {
self.current_session
}
pub(crate) fn advisory_holder(&self, key: i64) -> Option<u32> {
self.advisory_locks.get(&key).map(|(owner, _)| *owner)
}
pub(crate) fn advisory_unlock_all_count(&mut self) -> i32 {
let me = self.current_session;
let mut n: i32 = 0;
for (_, (owner, depth)) in &self.advisory_locks {
if *owner == me {
n = n.saturating_add(*depth as i32);
}
}
self.advisory_locks.retain(|_, (owner, _)| *owner != me);
n
}
#[must_use]
pub const fn with_clock(mut self, clock: ClockFn) -> Self {
self.clock = Some(clock);
self
}
#[must_use]
pub const fn with_salt_fn(mut self, f: SaltFn) -> Self {
self.salt_fn = Some(f);
self
}
#[must_use]
pub fn with_env_cfg(mut self, env_cfg: testkit::EnvConfig) -> Self {
self.env_cfg = env_cfg;
self
}
pub fn env_cfg(&self) -> &testkit::EnvConfig {
&self.env_cfg
}
pub fn rng_seed(&self) -> u64 {
if let Some(seed) = self.env_cfg.random_seed {
return seed;
}
match self.clock {
Some(f) => f() as u64,
None => 0xBAD_5EED_DEAD_BEEF,
}
}
pub fn enter_injection_scope(&self) -> crate::testkit::injection::InjectionGuard {
#[cfg(feature = "injection-points")]
{
crate::testkit::injection::enter_scope(&self.injection_store)
}
#[cfg(not(feature = "injection-points"))]
{
crate::testkit::injection::new_guard()
}
}
#[cfg(feature = "injection-points")]
pub fn injection_store(&self) -> alloc::sync::Arc<crate::testkit::injection::InjectionStore> {
self.injection_store.clone()
}
#[must_use]
pub const fn with_max_query_rows(mut self, n: usize) -> Self {
self.max_query_rows = Some(n);
self
}
#[must_use]
pub const fn with_max_query_bytes(mut self, n: usize) -> Self {
self.max_query_bytes = Some(n);
self
}
pub const fn catalog(&self) -> &Catalog {
&self.catalog
}
pub fn snapshot_data(&self) -> EngineSnapshot {
EngineSnapshot {
catalog: self.catalog.clone(),
users: self.users.clone(),
publications: self.publications.clone(),
subscriptions: self.subscriptions.clone(),
statistics: self.statistics.clone(),
}
}
pub fn snapshot(&self) -> Vec<u8> {
self.snapshot_data().serialize()
}
pub fn in_transaction(&self) -> bool {
!self.tx_catalogs.is_empty()
}
pub fn is_tx_open(&self, tx_id: TxId) -> bool {
self.tx_catalogs.contains_key(&tx_id)
}
pub(crate) fn effective_users(&self) -> &crate::users::UserStore {
if let Some(tx) = self.current_tx
&& let Some(state) = self.tx_catalogs.get(&tx)
&& let Some(shadow) = &state.users
{
return shadow;
}
&self.users
}
pub(crate) fn role_ddl_users_mut(&mut self) -> &mut crate::users::UserStore {
let tx_slot = self
.current_tx
.filter(|tx| self.tx_catalogs.contains_key(tx));
match tx_slot {
Some(tx) => {
if self
.tx_catalogs
.get(&tx)
.is_some_and(|state| state.users.is_none())
{
let committed = self.users.clone();
if let Some(state) = self.tx_catalogs.get_mut(&tx) {
state.users = Some(committed);
}
}
self.tx_catalogs
.get_mut(&tx)
.and_then(|state| state.users.as_mut())
.expect("role shadow ensured just above for an open tx slot")
}
None => &mut self.users,
}
}
pub fn alloc_tx_id(&mut self) -> TxId {
let id = TxId(self.next_tx_id);
self.next_tx_id = self.next_tx_id.saturating_add(1);
id
}
pub fn replace_catalog(&mut self, catalog: Catalog) {
self.catalog = catalog;
}
pub fn freeze_oldest_to_cold(
&mut self,
table_name: &str,
index_name: &str,
max_rows: usize,
) -> Result<spg_storage::FreezeReport, EngineError> {
let report = self
.active_catalog_mut()
.freeze_oldest_to_cold(table_name, index_name, max_rows)
.map_err(EngineError::Storage)?;
if let Some(t) = self.active_catalog_mut().get_mut(table_name) {
t.mark_cold_row_count_stale();
}
Ok(report)
}
pub fn receive_cold_segment(
&mut self,
segment_id: u32,
bytes: Vec<u8>,
) -> Result<(), EngineError> {
let mut new_cat = self.catalog.clone();
match new_cat.load_segment_bytes_at(segment_id, bytes) {
Ok(()) => {
self.replace_catalog(new_cat);
Ok(())
}
Err(StorageError::Corrupt(msg)) if msg.contains("already occupied") => Ok(()),
Err(e) => Err(EngineError::Storage(e)),
}
}
pub(crate) fn base_catalog_mut(&mut self) -> &mut Catalog {
&mut self.catalog
}
pub(crate) fn active_catalog(&self) -> &Catalog {
match self.current_tx {
Some(t) => self
.tx_catalogs
.get(&t)
.map_or(&self.catalog, |s| &s.catalog),
None => &self.catalog,
}
}
fn active_catalog_mut(&mut self) -> &mut Catalog {
let tx = self.current_tx;
match tx {
Some(t) => match self.tx_catalogs.get_mut(&t) {
Some(s) => {
s.shadow_dirty = true;
&mut s.catalog
}
None => &mut self.catalog,
},
None => &mut self.catalog,
}
}
pub fn set_redo_capture(&mut self, on: bool) {
self.redo_capture = on;
}
pub(crate) const MATVIEW_DELTA_CEILING: usize = 65_536;
pub(crate) fn fan_out_matview_deltas(&mut self, drained: &[RowChange]) {
if self.matview_maintainable.is_empty() {
return;
}
for ch in drained {
let t = ch.table_name().to_ascii_lowercase();
let hit: Vec<String> = self
.matview_maintainable
.iter()
.filter(|(_, base)| **base == t)
.map(|(mv, _)| mv.clone())
.collect();
for mv in hit {
if self.matview_delta_overflow.contains(&mv) {
continue;
}
let buf = self.matview_delta_buf.entry(mv.clone()).or_default();
if buf.len() >= Self::MATVIEW_DELTA_CEILING {
self.matview_delta_overflow.insert(mv.clone());
self.matview_delta_buf.remove(&mv);
} else {
buf.push(ch.clone());
MATVIEW_FANOUT_BUFFERED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}
}
}
}
pub(crate) fn bump_table_change(&mut self, table: &str) {
let k = table.to_ascii_lowercase();
*self.table_change_seq.entry(k).or_insert(0) += 1;
}
pub fn redo_capture_enabled(&self) -> bool {
self.redo_capture
}
pub fn current_isolation_level(&self) -> spg_sql::ast::IsolationLevel {
self.current_isolation_level
}
pub fn take_redo(&mut self) -> Vec<RowChange> {
core::mem::take(&mut self.last_redo)
}
pub fn apply_redo(&mut self, changes: &[RowChange]) -> Result<(), EngineError> {
self.catalog
.apply_redo(changes)
.map_err(EngineError::Storage)
}
fn enforce_row_limit(
&self,
result: Result<QueryResult, EngineError>,
) -> Result<QueryResult, EngineError> {
if let Ok(QueryResult::Rows { rows, .. }) = &result {
if let Some(cap) = self.max_query_rows
&& rows.len() > cap
{
return Err(EngineError::RowLimitExceeded(cap));
}
if let Some(byte_cap) = self.max_query_bytes
&& approx_rows_bytes(rows) > byte_cap
{
return Err(EngineError::QueryBytesExceeded(byte_cap));
}
}
result
}
}
#[derive(Debug, Clone)]
pub struct TableMemoryStats {
pub name: String,
pub hot_rows: u64,
pub cold_rows: u64,
pub hot_encoded_bytes: u64,
pub approx_resident_bytes: u64,
pub index_count: u64,
pub approx_index_bytes: u64,
}
#[derive(Debug, Clone)]
pub struct MemoryStats {
pub tables: Vec<TableMemoryStats>,
pub total_hot_encoded_bytes: u64,
pub total_approx_resident_bytes: u64,
pub total_approx_index_bytes: u64,
pub max_query_bytes: Option<usize>,
pub wal_bytes: Option<u64>,
}
const fn is_internal_table_name(_name: &str) -> bool {
false
}
#[cfg(test)]
mod tests;