use pgrx::guc::{GucContext, GucFlags, GucRegistry, GucSetting};
use pgrx::prelude::PostgresGucEnum;
pub const MAX_DEPENDENCY_DEPTH: usize = 10;
pub const DEBUG_DEPENDENCIES: bool = false;
#[derive(PostgresGucEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum UncascadedPolicy {
#[name = c"warn"]
Warn,
#[name = c"error"]
Error,
#[name = c"full_refresh"]
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 from_stored(name: &str) -> Self {
match name {
"error" => Self::Error,
"full_refresh" => Self::FullRefresh,
_ => Self::Warn,
}
}
}
static MAX_PROPAGATION_DEPTH_GUC: GucSetting<i32> = GucSetting::<i32>::new(100);
static GRAPH_CACHE_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
static TABLE_CACHE_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
static METRICS_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
static LOG_LEVEL_GUC: GucSetting<Option<std::ffi::CString>> =
GucSetting::<Option<std::ffi::CString>>::new(Some(c"info"));
static UNION_DUPLICATE_POLICY_GUC: GucSetting<Option<std::ffi::CString>> =
GucSetting::<Option<std::ffi::CString>>::new(Some(c"error"));
static MAX_QUEUE_SIZE_GUC: GucSetting<i32> = GucSetting::<i32>::new(10_000);
static AUDIT_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
static UNLOGGED_BY_DEFAULT_GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
static TEST_SKIP_CTAS_INTERCEPT_GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
static SUSPEND_TRIGGERS_GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
static MAX_DEPENDENCY_DEPTH_GUC: GucSetting<i32> = GucSetting::<i32>::new(10);
static BATCH_SIZE_GUC: GucSetting<i32> = GucSetting::<i32>::new(1_000);
static CACHE_SIZE_GUC: GucSetting<i32> = GucSetting::<i32>::new(10_000);
static DIRECT_PATCH_ENABLED_GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
static DATA_GIN_INDEX_GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
static FILLFACTOR_GUC: GucSetting<i32> = GucSetting::<i32>::new(85);
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(None);
static UNCASCADED_POLICY_GUC: GucSetting<UncascadedPolicy> =
GucSetting::<UncascadedPolicy>::new(UncascadedPolicy::Warn);
pub fn register_gucs() {
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::Userset,
GucFlags::default(),
);
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::Userset,
GucFlags::default(),
);
GucRegistry::define_bool_guc(
c"pg_tviews.table_cache_enabled",
c"Enable in-memory caching of table OID to entity name mappings.",
c"When false, entity lookups query pg_tview_meta on every trigger.",
&TABLE_CACHE_ENABLED_GUC,
GucContext::Userset,
GucFlags::default(),
);
GucRegistry::define_bool_guc(
c"pg_tviews.metrics_enabled",
c"Deprecated: has no effect; will be removed.",
c"Refresh metrics are always collected (pg_tviews_queue_stats()).",
&METRICS_ENABLED_GUC,
GucContext::Userset,
GucFlags::default(),
);
GucRegistry::define_string_guc(
c"pg_tviews.log_level",
c"Logging verbosity for pg_tviews operations.",
c"Set to 'debug' to show internal diagnostics (event trigger, DDL tracing) as NOTICE; otherwise they are DEBUG1 messages.",
&LOG_LEVEL_GUC,
GucContext::Userset,
GucFlags::default(),
);
GucRegistry::define_enum_guc(
c"pg_tviews.uncascaded_policy",
c"What a new TVIEW does about base tables whose writes no cascade reaches.",
c"warn: WARNING, rows stay stale on such writes; error: refuse the TVIEW; \
full_refresh: such writes refresh the whole TVIEW. Read once at create time \
and stored with the TVIEW.",
&UNCASCADED_POLICY_GUC,
GucContext::Userset,
GucFlags::default(),
);
GucRegistry::define_string_guc(
c"pg_tviews.union_duplicate_policy",
c"Policy when a UNION ALL backing view returns multiple rows for the same key.",
c"Allowed values: 'first' (silently take first row), 'error' (abort transaction).",
&UNION_DUPLICATE_POLICY_GUC,
GucContext::Userset,
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, 1_000_000, GucContext::Userset,
GucFlags::default(),
);
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::Userset,
GucFlags::NO_SHOW_ALL,
);
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::Userset,
GucFlags::default(),
);
GucRegistry::define_bool_guc(
c"pg_tviews.unlogged_by_default",
c"Create TVIEW tables as UNLOGGED by default.",
c"When true, new TVIEWs are created as UNLOGGED tables for better write performance. \
A hot standby cannot read an UNLOGGED table, and promotion or a crash restart \
empties it: turn this off for TVIEWs served from replicas.",
&UNLOGGED_BY_DEFAULT_GUC,
GucContext::Userset,
GucFlags::default(),
);
GucRegistry::define_bool_guc(
c"pg_tviews.data_gin_index",
c"Create a GIN index on the data column of new TVIEWs.",
c"Off by default: nearly every refresh rewrites data, so an index on it makes \
every refresh a non-HOT update. Enable per TVIEW (SET LOCAL) only when \
top-level containment queries (data @> ...) need it.",
&DATA_GIN_INDEX_GUC,
GucContext::Userset,
GucFlags::default(),
);
GucRegistry::define_int_guc(
c"pg_tviews.fillfactor",
c"Heap fillfactor for new TVIEW tables.",
c"Free space kept on each page so refreshes can place the new row version on \
the same page (HOT update). 100 packs pages fully, for append-mostly TVIEWs.",
&FILLFACTOR_GUC,
10, 100, GucContext::Userset,
GucFlags::default(),
);
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"Comma-separated database names. For each one a background worker runs \
pg_tviews_rebuild_all() at startup, after a crash restart and on promotion. \
Empty (the default) starts no worker. Requires a server restart.",
&AUTO_REBUILD_DATABASES_GUC,
GucContext::Postmaster,
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(),
);
GucRegistry::define_bool_guc(
c"pg_tviews.suspend_triggers",
c"Suspend trigger-based refresh during bulk operations.",
c"When true, row-level triggers will not enqueue refresh tasks.",
&SUSPEND_TRIGGERS_GUC,
GucContext::Userset,
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::Userset,
GucFlags::default(),
);
GucRegistry::define_int_guc(
c"pg_tviews.batch_size",
c"Maximum primary 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_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 (issue #56).",
&DIRECT_PATCH_ENABLED_GUC,
GucContext::Userset,
GucFlags::default(),
);
unsafe { pgrx::pg_sys::MarkGUCPrefixReserved(c"pg_tviews".as_ptr()) };
}
#[must_use]
pub fn max_propagation_depth() -> usize {
MAX_PROPAGATION_DEPTH_GUC.get().unsigned_abs() as usize
}
#[must_use]
pub fn auto_rebuild_databases() -> Vec<String> {
AUTO_REBUILD_DATABASES_GUC
.get()
.and_then(|s| s.to_str().ok().map(str::to_string))
.map(|s| {
s.split(',')
.map(str::trim)
.filter(|d| !d.is_empty())
.map(str::to_string)
.collect()
})
.unwrap_or_default()
}
#[must_use]
pub fn report_max_tracked() -> usize {
usize::try_from(REPORT_MAX_TRACKED_GUC.get()).unwrap_or(0)
}
#[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 log_level() -> String {
LOG_LEVEL_GUC.get().map_or_else(
|| "info".to_owned(),
|cstr| cstr.to_str().unwrap_or("info").to_owned(),
)
}
pub fn warn_deprecated_settings() {
if METRICS_ENABLED_GUC.get() {
crate::utils::log_once(
"metrics_enabled deprecated",
"pg_tviews.metrics_enabled is deprecated and has no effect (metrics are always \
collected); it will be removed",
);
}
}
#[must_use]
pub fn union_duplicate_policy() -> String {
UNION_DUPLICATE_POLICY_GUC.get().map_or_else(
|| "error".to_owned(),
|cstr| cstr.to_str().unwrap_or("error").to_owned(),
)
}
#[must_use]
pub fn max_queue_size() -> usize {
MAX_QUEUE_SIZE_GUC.get().unsigned_abs() as usize
}
#[must_use]
pub fn audit_enabled() -> bool {
AUDIT_ENABLED_GUC.get()
}
#[must_use]
pub fn test_skip_ctas_intercept() -> bool {
TEST_SKIP_CTAS_INTERCEPT_GUC.get()
}
#[must_use]
pub fn unlogged_by_default() -> bool {
UNLOGGED_BY_DEFAULT_GUC.get()
}
#[must_use]
pub fn data_gin_index() -> bool {
DATA_GIN_INDEX_GUC.get()
}
#[must_use]
pub fn fillfactor() -> i32 {
FILLFACTOR_GUC.get()
}
#[must_use]
pub fn suspend_triggers() -> bool {
SUSPEND_TRIGGERS_GUC.get()
}
#[must_use]
pub fn max_dependency_depth() -> usize {
MAX_DEPENDENCY_DEPTH_GUC.get().unsigned_abs() as usize
}
#[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 direct_patch_enabled() -> bool {
DIRECT_PATCH_ENABLED_GUC.get()
}
pub fn uncascaded_policy() -> UncascadedPolicy {
UNCASCADED_POLICY_GUC.get()
}