use pgrx::guc::{GucContext, GucFlags, GucRegistry, GucSetting};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum UncascadedPolicy {
Warn,
#[default]
Error,
FullRefresh,
}
impl UncascadedPolicy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Warn => "warn",
Self::Error => "error",
Self::FullRefresh => "full_refresh",
}
}
#[must_use]
pub fn parse(name: &str) -> Option<Self> {
match name {
"warn" => Some(Self::Warn),
"error" => Some(Self::Error),
"full_refresh" => Some(Self::FullRefresh),
_ => None,
}
}
}
static MAX_PROPAGATION_DEPTH_GUC: GucSetting<i32> = GucSetting::<i32>::new(100);
static MAX_DEPENDENCY_DEPTH_GUC: GucSetting<i32> = GucSetting::<i32>::new(10);
static MAX_QUEUE_SIZE_GUC: GucSetting<i32> = GucSetting::<i32>::new(1_000_000);
static MAX_REFRESH_ROWS_GUC: GucSetting<i32> = GucSetting::<i32>::new(0);
static PUBLISH_CHANGES_GUC: GucSetting<PublishChanges> =
GucSetting::<PublishChanges>::new(PublishChanges::Off);
#[derive(pgrx::PostgresGucEnum, Clone, Copy, PartialEq, Eq, Debug)]
pub enum PublishChanges {
#[name = c"off"]
Off,
#[name = c"types"]
Types,
#[name = c"rows"]
Rows,
}
static LOCK_ESCALATION_THRESHOLD_GUC: GucSetting<i32> = GucSetting::<i32>::new(64);
static AUDIT_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
static GRAPH_CACHE_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
static TABLE_CACHE_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
static DIRECT_PATCH_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
static TEST_SKIP_CTAS_INTERCEPT_GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
static BATCH_SIZE_GUC: GucSetting<i32> = GucSetting::<i32>::new(1_000);
static CACHE_SIZE_GUC: GucSetting<i32> = GucSetting::<i32>::new(10_000);
static REPORT_MAX_TRACKED_GUC: GucSetting<i32> = GucSetting::<i32>::new(10_000);
static AUTO_REBUILD_DATABASES_GUC: GucSetting<Option<std::ffi::CString>> =
GucSetting::<Option<std::ffi::CString>>::new(Some(c"*"));
pub fn register_gucs() {
register_limits();
register_diagnostics();
register_session_settings();
register_postmaster_gucs();
unsafe { pgrx::pg_sys::MarkGUCPrefixReserved(c"pg_tviews".as_ptr()) };
}
fn register_limits() {
GucRegistry::define_int_guc(
c"pg_tviews.max_propagation_depth",
c"Maximum cascade propagation iterations before aborting.",
c"Prevents infinite loops in circular dependency chains.",
&MAX_PROPAGATION_DEPTH_GUC,
1, 10_000, GucContext::Suset,
GucFlags::default(),
);
GucRegistry::define_int_guc(
c"pg_tviews.max_dependency_depth",
c"Maximum pg_depend traversal depth when building the dependency graph.",
c"Bounds how deep view-on-view hierarchies may nest before an error is raised.",
&MAX_DEPENDENCY_DEPTH_GUC,
1, 100, GucContext::Suset,
GucFlags::default(),
);
GucRegistry::define_int_guc(
c"pg_tviews.max_queue_size",
c"Maximum number of refresh items allowed in the transaction queue.",
c"When exceeded, new refresh enqueues raise an error to prevent unbounded queue growth.",
&MAX_QUEUE_SIZE_GUC,
1, i32::MAX, GucContext::Suset,
GucFlags::default(),
);
GucRegistry::define_enum_guc(
c"pg_tviews.publish_changes",
c"Publish the TVIEW rows each transaction changed, at commit, on channel tviews_changes.",
c"off: nothing. types: the changed GraphQL types. rows: the changed rows' ids, by type \
(types past a cap). See ADR 0231.",
&PUBLISH_CHANGES_GUC,
GucContext::Suset,
GucFlags::default(),
);
GucRegistry::define_int_guc(
c"pg_tviews.max_refresh_rows",
c"Maximum TVIEW rows one statement's refresh may write (0: no limit).",
c"A statement whose refresh would write more fails with 54000 before writing them. \
Recomputed keys, patched rows, fan-out children and a full refresh's rows are \
counted; explicit rebuilds are not.",
&MAX_REFRESH_ROWS_GUC,
0,
i32::MAX,
GucContext::Suset,
GucFlags::default(),
);
GucRegistry::define_int_guc(
c"pg_tviews.lock_escalation_threshold",
c"Value locks a transaction takes on one relation before it locks the relation instead.",
c"Counted per relation and per side (writes, refreshes). 0 always locks relations; \
-1 never does, which can exhaust the shared lock table on bulk writes.",
&LOCK_ESCALATION_THRESHOLD_GUC,
-1,
1_000_000,
GucContext::Suset,
GucFlags::default(),
);
GucRegistry::define_bool_guc(
c"pg_tviews.audit_enabled",
c"Enable audit logging of TVIEW operations to pg_tview_audit_log.",
c"When false, refresh/create/drop operations are not logged.",
&AUDIT_ENABLED_GUC,
GucContext::Suset,
GucFlags::default(),
);
}
fn register_diagnostics() {
GucRegistry::define_bool_guc(
c"pg_tviews.graph_cache_enabled",
c"Enable in-memory caching of entity dependency graphs.",
c"When false, graphs are loaded from pg_tview_meta on every refresh.",
&GRAPH_CACHE_ENABLED_GUC,
GucContext::Suset,
GucFlags::NO_SHOW_ALL,
);
GucRegistry::define_bool_guc(
c"pg_tviews.table_cache_enabled",
c"Enable in-memory caching of TVIEW catalog rows and their plans.",
c"When false, every lookup reads pg_tview_meta.",
&TABLE_CACHE_ENABLED_GUC,
GucContext::Suset,
GucFlags::NO_SHOW_ALL,
);
GucRegistry::define_bool_guc(
c"pg_tviews.direct_patch_enabled",
c"Enable the direct-patch fast path for eligible single-row UPDATEs.",
c"When true (default), an UPDATE whose changed columns all map identity-style \
to JSONB keys patches tv_<entity> directly, skipping the backing-view \
recompute. When false, every change takes the recompute path. Purely a \
performance switch: results are identical either way.",
&DIRECT_PATCH_ENABLED_GUC,
GucContext::Suset,
GucFlags::NO_SHOW_ALL,
);
GucRegistry::define_bool_guc(
c"pg_tviews.test_skip_ctas_intercept",
c"TEST ONLY: make the ProcessUtility hook skip CREATE TABLE tv_* AS interception.",
c"Simulates a session where the hook did not see the statement, to test the \
missed-interception error. Never enable in production.",
&TEST_SKIP_CTAS_INTERCEPT_GUC,
GucContext::Suset,
GucFlags::NO_SHOW_ALL,
);
}
fn register_session_settings() {
GucRegistry::define_int_guc(
c"pg_tviews.batch_size",
c"Maximum keys processed per statement during bulk refresh.",
c"Large multi-row changes are chunked into batches of this size to bound \
statement size and memory on very large bulk operations.",
&BATCH_SIZE_GUC,
1, 1_000_000, GucContext::Userset,
GucFlags::default(),
);
GucRegistry::define_int_guc(
c"pg_tviews.cache_size",
c"Maximum entries kept in each in-memory metadata cache.",
c"When a cache exceeds this many entries it is cleared and repopulated \
lazily, bounding per-backend memory on high-cardinality workloads.",
&CACHE_SIZE_GUC,
1, 10_000_000, GucContext::Userset,
GucFlags::default(),
);
GucRegistry::define_int_guc(
c"pg_tviews.report_max_tracked",
c"Changed TVIEW rows journaled per transaction for pg_tviews_flush_and_report().",
c"Beyond it only the entity types are kept and the report is marked truncated. \
0 turns the journal off.",
&REPORT_MAX_TRACKED_GUC,
0,
10_000_000,
GucContext::Userset,
GucFlags::default(),
);
}
fn register_postmaster_gucs() {
if unsafe { pgrx::pg_sys::process_shared_preload_libraries_in_progress } {
GucRegistry::define_string_guc(
c"pg_tviews.auto_rebuild_databases",
c"Databases whose emptied UNLOGGED TVIEWs are rebuilt once recovery finishes.",
c"* (the default): every database that accepts connections; a comma-separated \
list: only those; empty: none. A launcher starts a worker per database running \
pg_tviews_rebuild_all() at startup, after a crash restart and on promotion. \
Requires a server restart.",
&AUTO_REBUILD_DATABASES_GUC,
GucContext::Postmaster,
GucFlags::default(),
);
}
}
#[must_use]
pub fn max_propagation_depth() -> usize {
MAX_PROPAGATION_DEPTH_GUC.get().unsigned_abs() as usize
}
#[must_use]
pub fn max_dependency_depth() -> usize {
MAX_DEPENDENCY_DEPTH_GUC.get().unsigned_abs() as usize
}
#[must_use]
pub fn max_queue_size() -> usize {
MAX_QUEUE_SIZE_GUC.get().unsigned_abs() as usize
}
#[must_use]
pub fn publish_changes() -> Option<crate::queue::published::Publish> {
match PUBLISH_CHANGES_GUC.get() {
PublishChanges::Off => None,
PublishChanges::Types => Some(crate::queue::published::Publish::Types),
PublishChanges::Rows => Some(crate::queue::published::Publish::Rows),
}
}
#[must_use]
pub fn max_refresh_rows() -> Option<u64> {
match MAX_REFRESH_ROWS_GUC.get() {
0 => None,
max => Some(u64::from(max.unsigned_abs())),
}
}
pub fn lock_escalation_threshold() -> i32 {
LOCK_ESCALATION_THRESHOLD_GUC.get()
}
#[must_use]
pub fn audit_enabled() -> bool {
AUDIT_ENABLED_GUC.get()
}
#[must_use]
pub fn graph_cache_enabled() -> bool {
GRAPH_CACHE_ENABLED_GUC.get()
}
#[must_use]
pub fn table_cache_enabled() -> bool {
TABLE_CACHE_ENABLED_GUC.get()
}
#[must_use]
pub fn direct_patch_enabled() -> bool {
DIRECT_PATCH_ENABLED_GUC.get()
}
#[must_use]
pub fn test_skip_ctas_intercept() -> bool {
TEST_SKIP_CTAS_INTERCEPT_GUC.get()
}
#[must_use]
pub fn batch_size() -> usize {
BATCH_SIZE_GUC.get().unsigned_abs().max(1) as usize
}
#[must_use]
pub fn cache_size() -> usize {
CACHE_SIZE_GUC.get().unsigned_abs().max(1) as usize
}
#[must_use]
pub fn report_max_tracked() -> usize {
usize::try_from(REPORT_MAX_TRACKED_GUC.get()).unwrap_or(0)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RebuildDatabases {
All,
Only(Vec<String>),
None,
}
#[must_use]
pub fn rebuild_databases() -> RebuildDatabases {
let value = AUTO_REBUILD_DATABASES_GUC
.get()
.and_then(|s| s.to_str().ok().map(str::to_string))
.unwrap_or_default();
parse_rebuild_databases(&value)
}
fn parse_rebuild_databases(value: &str) -> RebuildDatabases {
let names: Vec<String> = value
.split(',')
.map(str::trim)
.filter(|d| !d.is_empty())
.map(str::to_string)
.collect();
match names.as_slice() {
[] => RebuildDatabases::None,
[all] if all == "*" => RebuildDatabases::All,
_ => RebuildDatabases::Only(names),
}
}
#[cfg(test)]
mod tests {
use super::{RebuildDatabases, parse_rebuild_databases};
#[test]
fn rebuild_databases_parse() {
assert_eq!(parse_rebuild_databases("*"), RebuildDatabases::All);
assert_eq!(parse_rebuild_databases(" "), RebuildDatabases::None);
assert_eq!(
parse_rebuild_databases("app, reporting"),
RebuildDatabases::Only(vec!["app".into(), "reporting".into()])
);
}
}