From ba90584c6e52278d9d7c4d346e6dcaeb8d27035c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 16:01:57 +0000 Subject: [PATCH 1/7] Quotas on readers' vectors: schema, decision, enforcement, SQL functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit graphs.reader_vector_quota, warren.vector_quotas (dumped with the extension, revoked from PUBLIC) and an index on vectors (graph_id, created_by); warren.set_vector_quota and warren.vector_usage; info reports reader_vector_quota. Every persisted write (Vectors::save) is admitted by vector::quota::admit: members of the owner role are exempt; where a quota applies, it takes a transaction-scoped advisory-type lock on (graph, role) with pg_warren's own objsubid, sums the role's usage under the latest snapshot and refuses with 53400 only a write that takes usage past the quota and above what it was (DESIGN.md §5.7, §14 items 46 and 47). The upgrade script from 0.5.0 adds the schema and functions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke --- crates/pg_warren/src/access/guard.rs | 6 +- crates/pg_warren/src/catalog/bootstrap.rs | 60 +- crates/pg_warren/src/catalog/info.rs | 7 +- crates/pg_warren/src/compute_sql/vectors.rs | 16 +- crates/pg_warren/src/parallel_labels.rs | 3 + crates/pg_warren/src/read_only.rs | 13 +- crates/pg_warren/src/vector/error.rs | 5 + crates/pg_warren/src/vector/mod.rs | 5 +- crates/pg_warren/src/vector/quota/enforce.rs | 326 +++ crates/pg_warren/src/vector/quota/error.rs | 64 + crates/pg_warren/src/vector/quota/mod.rs | 94 + crates/pg_warren/src/vector/quota/sql.rs | 198 ++ sql/pg_warren--0.5.0--0.6.0.sql | 24 + sql/pg_warren--0.6.0.sql | 2494 +++++++++--------- 14 files changed, 2064 insertions(+), 1251 deletions(-) create mode 100644 crates/pg_warren/src/vector/quota/enforce.rs create mode 100644 crates/pg_warren/src/vector/quota/error.rs create mode 100644 crates/pg_warren/src/vector/quota/mod.rs create mode 100644 crates/pg_warren/src/vector/quota/sql.rs diff --git a/crates/pg_warren/src/access/guard.rs b/crates/pg_warren/src/access/guard.rs index 952a179f..91f2a615 100644 --- a/crates/pg_warren/src/access/guard.rs +++ b/crates/pg_warren/src/access/guard.rs @@ -300,6 +300,7 @@ mod tests { ("revoke", "warren.revoke('victim', 'eve')"), ("s", "warren.s(NULL)"), ("set_owner", "warren.set_owner('victim', 'eve')"), + ("set_vector_quota", "warren.set_vector_quota('victim', 1000000, 'eve')"), ("shared_cache", "warren.shared_cache()"), ("shortest_path", "warren.shortest_path('victim', 1::bigint, 2::bigint)"), ("similar_pairs", "warren.similar_pairs('victim', NULL::bigint)"), @@ -320,6 +321,7 @@ mod tests { ("vector_reduce", "warren.vector_reduce('victim', 'c', 'sum')"), ("vector_stats", "warren.vector_stats('victim', 'c')"), ("vector_topk", "warren.vector_topk('victim', 'c', 3, NULL::bigint)"), + ("vector_usage", "warren.vector_usage('victim')"), ("vector_values", "warren.vector_values('victim', 'c', NULL::bigint)"), ("version", "warren.version()"), ("walk", "warren.walk('victim', ARRAY[1::bigint], ARRAY[warren.s(NULL)])"), @@ -369,10 +371,12 @@ mod tests { // Label vector `c` and quotient `victim_q`, which eve reads, so // that contract, project and lift run all the way as eve; and // `eve_minion`, whose privileges eve has, so that drop_owned and - // reassign_owned run all the way too (it owns nothing). + // reassign_owned run all the way too (it owns nothing); and a + // quota on readers' vectors, so that eve's writes are counted. Spi::run( "CREATE ROLE eve_minion; GRANT eve_minion TO eve; SELECT warren.grant('victim', 'eve'); + SELECT warren.set_vector_quota('victim', 1000000); SELECT warren.vector_const('victim', 'c', 1); SELECT warren.contract('victim', 'c', 'victim_q'); SELECT warren.vector_const('victim_q', 'c', 1); diff --git a/crates/pg_warren/src/catalog/bootstrap.rs b/crates/pg_warren/src/catalog/bootstrap.rs index 6dcb3db4..ddddbe04 100644 --- a/crates/pg_warren/src/catalog/bootstrap.rs +++ b/crates/pg_warren/src/catalog/bootstrap.rs @@ -1,6 +1,7 @@ //! The catalog tables, created by the extension script (DESIGN.md §5.1): -//! `warren.graphs`, `versions`, `segments`, `vectors` and `vector_chunks`, -//! revoked from `PUBLIC` and registered with `pg_extension_config_dump`. +//! `warren.graphs`, `versions`, `segments`, `vectors`, `vector_chunks` +//! and `vector_quotas`, revoked from `PUBLIC` and registered with +//! `pg_extension_config_dump`. // Emitted at the end of the script (`finalize`), after `CREATE SCHEMA warren`. // Functions reach these tables only at run time, through SPI. @@ -25,7 +26,8 @@ CREATE TABLE warren.graphs ( current_version bigint, store_merged boolean NOT NULL DEFAULT true, node_order text NOT NULL DEFAULT 'id' - CHECK (node_order IN ('id'::pg_catalog.text, 'locality'::pg_catalog.text)) + CHECK (node_order IN ('id'::pg_catalog.text, 'locality'::pg_catalog.text)), + reader_vector_quota bigint CHECK (reader_vector_quota >= 0) ); CREATE TABLE warren.versions ( @@ -79,6 +81,17 @@ CREATE TABLE warren.vector_chunks ( ); -- IEEE floats rarely compress, so skip pglz's attempt on every write. ALTER TABLE warren.vector_chunks ALTER COLUMN data SET STORAGE EXTERNAL; +-- A quota sums one role's vectors on one graph (DESIGN.md §5.7). +CREATE INDEX vectors_created_by ON warren.vectors (graph_id, created_by); + +-- Per-role quotas on readers' persisted vectors, overriding +-- graphs.reader_vector_quota (DESIGN.md §5.7, §14 item 46). +CREATE TABLE warren.vector_quotas ( + graph_id int NOT NULL REFERENCES warren.graphs ON DELETE CASCADE, + role regrole NOT NULL, + bytes bigint NOT NULL CHECK (bytes >= 0), + PRIMARY KEY (graph_id, role) +); -- graphs.current_version is not a foreign key: with versions.graph_id it -- would make a cycle, and pg_dump cannot order the data of tables in a @@ -164,6 +177,7 @@ SELECT pg_catalog.pg_extension_config_dump('warren.segments', ''); SELECT pg_catalog.pg_extension_config_dump('warren.vectors', ''); SELECT pg_catalog.pg_extension_config_dump('warren.vectors_vector_id_seq', ''); SELECT pg_catalog.pg_extension_config_dump('warren.vector_chunks', ''); +SELECT pg_catalog.pg_extension_config_dump('warren.vector_quotas', ''); "#, name = "catalog", finalize, @@ -231,6 +245,37 @@ mod tests { Ok(()) } + /// A quota sums one role's vectors on one graph (DESIGN.md §5.7). + #[pg_test] + fn vectors_are_indexed_by_graph_and_creator() -> Result<(), Error> { + let index = Spi::get_one::( + "SELECT pg_get_indexdef('warren.vectors_created_by'::regclass)", + )?; + assert_eq!( + index.as_deref(), + Some("CREATE INDEX vectors_created_by ON warren.vectors USING btree (graph_id, created_by)") + ); + Ok(()) + } + + #[pg_test] + fn quotas_are_bytes_not_negative() -> Result<(), Error> { + ring("quota_check", 3, 2)?; + assert_eq!( + sqlstate("UPDATE warren.graphs SET reader_vector_quota = -1")?.as_deref(), + Some("23514") + ); + assert_eq!( + sqlstate( + "INSERT INTO warren.vector_quotas SELECT graph_id, 'quota_check_owner', -1 + FROM warren.graphs" + )? + .as_deref(), + Some("23514") + ); + Ok(()) + } + #[pg_test] fn versions_are_indexed_by_parent_and_label_vector() -> Result<(), Error> { let indexes = Spi::get_one::( @@ -293,7 +338,7 @@ mod tests { build_reverse boolean not null, weighted boolean not null, \ has_relations boolean not null, retain_versions integer not null, \ current_version bigint, store_merged boolean not null, \ - node_order text not null" + node_order text not null, reader_vector_quota bigint" ) ); Ok(()) @@ -338,6 +383,10 @@ mod tests { columns("vector_chunks")?.as_deref(), Some("vector_id bigint not null, chunk_no integer not null, data bytea not null") ); + assert_eq!( + columns("vector_quotas")?.as_deref(), + Some("graph_id integer not null, role regrole not null, bytes bigint not null") + ); Ok(()) } @@ -374,7 +423,8 @@ mod tests { dumped.as_deref(), Some( "warren.graphs, warren.graphs_graph_id_seq, warren.segments, \ - warren.vector_chunks, warren.vectors, warren.vectors_vector_id_seq, \ + warren.vector_chunks, warren.vector_quotas, warren.vectors, \ + warren.vectors_vector_id_seq, \ warren.versions, warren.versions_version_id_seq" ) ); diff --git a/crates/pg_warren/src/catalog/info.rs b/crates/pg_warren/src/catalog/info.rs index f902c2ad..709233f8 100644 --- a/crates/pg_warren/src/catalog/info.rs +++ b/crates/pg_warren/src/catalog/info.rs @@ -9,7 +9,8 @@ use super::{CatalogError, GraphDefinition}; use crate::access::{as_catalog_owner, entry, is_reader, require_reader}; use crate::read_only::read_one; -/// `warren.info`: graph `name`'s definition, current version, staleness +/// `warren.info`: graph `name`'s definition (with its default quota on +/// readers' vectors, `reader_vector_quota`), current version, staleness /// (`now() - built_at` of the current version), total bytes, and every /// version with its statistics, oldest first. #[pg_extern(schema = "warren", stable, parallel_safe)] @@ -28,6 +29,7 @@ fn describe(name: &str) -> Result { 'has_relations', g.has_relations, 'build_reverse', g.build_reverse, 'store_merged', g.store_merged, 'node_order', g.node_order, 'retain_versions', g.retain_versions, 'current_version', g.current_version, + 'reader_vector_quota', g.reader_vector_quota, 'staleness', (SELECT (now() - c.built_at)::text FROM warren.versions c WHERE c.version_id = g.current_version AND c.graph_id = g.graph_id), @@ -133,6 +135,9 @@ mod tests { assert_eq!(info("{retain_versions}")?.as_deref(), Some("2")); assert_eq!(info("{store_merged}")?.as_deref(), Some("true")); assert_eq!(info("{node_order}")?.as_deref(), Some("\"id\"")); + assert_eq!(info("{reader_vector_quota}")?.as_deref(), Some("null")); + Spi::run("SELECT warren.set_vector_quota('i', 4096)")?; + assert_eq!(info("{reader_vector_quota}")?.as_deref(), Some("4096")); assert_eq!(info("{current_version}")?, Some(promoted.to_string())); assert_eq!(info("{versions,1,version}")?, Some(staged.to_string())); assert_eq!(info("{versions,0,current}")?.as_deref(), Some("true")); diff --git a/crates/pg_warren/src/compute_sql/vectors.rs b/crates/pg_warren/src/compute_sql/vectors.rs index a1840f13..a7c32b02 100644 --- a/crates/pg_warren/src/compute_sql/vectors.rs +++ b/crates/pg_warren/src/compute_sql/vectors.rs @@ -24,8 +24,8 @@ use crate::catalog::{GraphDefinition, VersionId}; use crate::segment::CHUNK_SIZE; use crate::source::IdDatum; use crate::vector::{ - drop_transient, keep_transient, transient, transients, write_vector, Binding, SpiVectorStore, - VectorError, VectorId, VectorRecord, VectorStore, + admit_to_quota, drop_transient, keep_transient, transient, transients, write_vector, Binding, + SpiVectorStore, VectorError, VectorId, VectorRecord, VectorStore, }; /// The budget cached vectors are charged to while cached: unlimited, since @@ -266,8 +266,10 @@ impl Vectors { /// any vector of that name of either kind; a persisted one replaced /// must have been written by the caller, or the caller must own the /// graph. A transient one is refused if it would take this backend's - /// transient vectors past `warren.max_cache_memory`; either kind is - /// kept in the cache only as far as the cap allows. + /// transient vectors past `warren.max_cache_memory`, a persisted one if + /// it would take the caller's persisted vectors on the graph past its + /// quota (DESIGN.md §5.7); either kind is kept in the cache only as far + /// as the cap allows. pub(crate) fn save( &mut self, name: &str, @@ -277,7 +279,11 @@ impl Vectors { self.check_writable(name, persist)?; let graph_id = self.graph.graph_id; let replacing = self.record(name).map(VectorRecord::checked); - if !persist { + if persist { + let written = vector_bytes(vector.dtype(), vector.width(), vector.id_space()); + let replaced = replacing.map(|old| old.vector); + admit_to_quota(&self.graph, name, replaced, written).map_err(VectorError::from)?; + } else { let replaced = transient(graph_id, self.version, name).map_or(0, |old| old.bytes()); admit_transient(replaced, vector.bytes())?; } diff --git a/crates/pg_warren/src/parallel_labels.rs b/crates/pg_warren/src/parallel_labels.rs index a7eab03d..09c72b33 100644 --- a/crates/pg_warren/src/parallel_labels.rs +++ b/crates/pg_warren/src/parallel_labels.rs @@ -90,6 +90,7 @@ mod tests { ("revoke", "u"), ("s", "s"), ("set_owner", "u"), + ("set_vector_quota", "u"), ("shared_cache", "s"), ("shortest_path", "s"), ("similar_pairs", "r"), @@ -110,6 +111,7 @@ mod tests { ("vector_reduce", "r"), ("vector_stats", "r"), ("vector_topk", "r"), + ("vector_usage", "s"), ("vector_values", "r"), ("version", "s"), ("walk", "s"), @@ -161,6 +163,7 @@ mod tests { "warren.vector_reduce('pp', 'c', 'sum')", "warren.vector_stats('pp', 'c')", "warren.vector_topk('pp', 'c', 3, NULL::bigint)", + "warren.vector_usage('pp')", "warren.vector_values('pp', 'c', NULL::bigint)", "warren.version()", "warren.walk('pp', ARRAY[1::bigint], ARRAY[warren.s(NULL)])", diff --git a/crates/pg_warren/src/read_only.rs b/crates/pg_warren/src/read_only.rs index e59a6991..9a37c546 100644 --- a/crates/pg_warren/src/read_only.rs +++ b/crates/pg_warren/src/read_only.rs @@ -11,9 +11,9 @@ //! parallel plan runs. It promises nothing about snapshots: as for any SPI //! read, once the transaction has written, pgrx runs the select //! non-read-only (a command counter increment and a fresh snapshot). -//! [`read_one_latest`] reads the latest committed state instead of the -//! statement's snapshot, for what must not depend on how old the caller's -//! snapshot is. Depends on no other feature. +//! [`read_one_latest`] reads the latest committed state (and the +//! transaction's own earlier writes) instead of the statement's snapshot, +//! for what must not depend on how old the caller's snapshot is. Depends on no other feature. use std::ffi::{c_char, CString}; use std::ptr; @@ -48,9 +48,10 @@ pub(crate) fn latest_readable() -> bool { /// The first column of the first row `query` returns, read-only, under the /// latest snapshot (what is committed now, not what the statement's -/// snapshot sees, nor this transaction's uncommitted writes), whatever the -/// isolation level and whether or not the transaction has written. Only -/// where [`latest_readable`]. +/// snapshot sees), whatever the isolation level and whether or not the +/// transaction has written. Like any snapshot, it also sees what this +/// transaction itself wrote in earlier commands, which a quota counts +/// (DESIGN.md §5.7). Only where [`latest_readable`]. pub(crate) fn read_one_latest( query: &str, args: &[DatumWithOid<'_>], diff --git a/crates/pg_warren/src/vector/error.rs b/crates/pg_warren/src/vector/error.rs index 91ff7602..a15520a4 100644 --- a/crates/pg_warren/src/vector/error.rs +++ b/crates/pg_warren/src/vector/error.rs @@ -4,6 +4,7 @@ use pgrx::PgSqlErrorCode; use thiserror::Error; use warren_core::compute::ComputeError; +use super::quota::QuotaError; use super::VectorId; use crate::access::{AccessError, SqlError}; use crate::catalog::VersionId; @@ -44,6 +45,9 @@ pub(crate) enum VectorError { /// out. #[error(transparent)] Compute(#[from] ComputeError), + /// A quota refused the write, or could not be read (DESIGN.md §5.7). + #[error(transparent)] + Quota(#[from] QuotaError), /// The switch to the catalog owner failed. #[error(transparent)] Access(#[from] AccessError), @@ -67,6 +71,7 @@ impl SqlError for VectorError { PgSqlErrorCode::ERRCODE_OUT_OF_MEMORY } Self::Compute(_) => PgSqlErrorCode::ERRCODE_DATA_CORRUPTED, + Self::Quota(error) => error.sqlstate(), Self::Access(error) => error.sqlstate(), Self::Spi(_) => PgSqlErrorCode::ERRCODE_INTERNAL_ERROR, } diff --git a/crates/pg_warren/src/vector/mod.rs b/crates/pg_warren/src/vector/mod.rs index 65450e9c..e3877c29 100644 --- a/crates/pg_warren/src/vector/mod.rs +++ b/crates/pg_warren/src/vector/mod.rs @@ -2,7 +2,8 @@ //! rows in `warren.vectors` and chunked bytes in `warren.vector_chunks`, //! behind a vector store port with an SPI adapter and an in-memory fake; //! the backend-local cache of loaded vectors, keyed by vector id; and -//! transient vectors, kept only in the backend (`persist := false`). +//! transient vectors, kept only in the backend (`persist := false`); and +//! quotas on what each reader role persists on a graph (`quota`). //! //! A vector row is never updated: writing a name again deletes the old row //! (its chunks cascade) and inserts a new one, so a vector id names one @@ -17,6 +18,7 @@ mod cache; mod carry; mod error; mod persist; +mod quota; mod spi; mod store; mod transient; @@ -28,6 +30,7 @@ pub(crate) use cache::{ pub(crate) use carry::{move_vectors, moved_notice}; pub(crate) use error::VectorError; pub(crate) use persist::{read_vector, write_vector}; +pub(crate) use quota::admit as admit_to_quota; pub(crate) use spi::SpiVectorStore; pub(crate) use store::{Binding, Checked, ChunkSource, VectorId, VectorRecord, VectorStore}; pub(crate) use transient::{ diff --git a/crates/pg_warren/src/vector/quota/enforce.rs b/crates/pg_warren/src/vector/quota/enforce.rs new file mode 100644 index 00000000..15806817 --- /dev/null +++ b/crates/pg_warren/src/vector/quota/enforce.rs @@ -0,0 +1,326 @@ +//! Enforcement before a persisted write (DESIGN.md §14 item 47): the +//! (graph, role) lock, the role's usage under the latest committed state, +//! and the decision. + +use pgrx::prelude::*; + +use super::{admits, QuotaError, Usage}; +use crate::access::{as_catalog_owner, is_owner, role_name}; +use crate::catalog::GraphDefinition; +use crate::read_only::read_one_latest; +use crate::vector::VectorId; + +/// pg_warren's `objsubid` (`locktag_field4`) on its advisory-type locks. +/// Applications' advisory locks have 1 (one `bigint` key) or 2 (two `int` +/// keys), so the quota's lock never collides with theirs; `pg_locks` shows +/// it as `advisory` with this `objsubid`. +pub(crate) const QUOTA_LOCK_OBJSUBID: u16 = 0x5751; + +/// The quota of role `$2` on graph `$1`: its own row, else the graph's +/// default (NULL for none). +const QUOTA: &str = "SELECT coalesce( + (SELECT q.bytes FROM warren.vector_quotas q WHERE q.graph_id = $1 AND q.role::oid = $2), + (SELECT g.reader_vector_quota FROM warren.graphs g WHERE g.graph_id = $1))"; + +/// The bytes of the persisted vectors role `$2` wrote on graph `$1`, and of +/// those among them that are vector `$3` (the one replaced, if any), as an +/// array of two: one scan of `vectors (graph_id, created_by)`. +const STORED: &str = concat!( + "SELECT ARRAY[coalesce(sum(", + vector_bytes!(), + "), 0), coalesce(sum(", + vector_bytes!(), + ") FILTER (WHERE v.vector_id = $3), 0)]::int8[] + FROM warren.vectors v WHERE v.graph_id = $1 AND v.created_by::oid = $2" +); + +/// Admits, or refuses with [`QuotaError::Exceeded`], the current role's +/// persisted write of vector `name` (`written` bytes) to `graph`, +/// replacing vector `replacing` (as the writer checked it), before +/// anything is written. Members of the owner role are exempt; where a +/// quota applies, the (graph, role) lock is held until the transaction +/// ends, so one role's writes to one graph are counted one at a time. +pub(crate) fn admit( + graph: &GraphDefinition, + name: &str, + replacing: Option, + written: u64, +) -> Result<(), QuotaError> { + let exempt = is_owner(graph.owner); + if exempt { + return Ok(()); + } + // SAFETY: reads the backend's current user id: the writing role. + let role = unsafe { pg_sys::GetUserId() }; + let Some(quota) = quota_of(graph.graph_id, role)? else { + return Ok(()); + }; + lock(graph.graph_id, role); + let (stored, replaced) = stored(graph.graph_id, role, replacing)?; + let usage = Usage::of(stored, replaced, written); + if admits(exempt, Some(quota), usage) { + return Ok(()); + } + Err(QuotaError::Exceeded { + vector: name.to_owned(), + written, + role: role_name(role).unwrap_or_default(), + graph: graph.name.clone(), + quota, + before: usage.before, + after: usage.after, + }) +} + +/// The quota of `role` on graph `graph_id`, as committed now. +fn quota_of(graph_id: i32, role: pg_sys::Oid) -> Result, QuotaError> { + let quota = as_catalog_owner(|| { + Ok::<_, QuotaError>(read_one_latest::(QUOTA, &[graph_id.into(), role.into()])?) + })?; + Ok(quota.map(|bytes| u64::try_from(bytes).unwrap_or_default())) +} + +/// What `role` stores on graph `graph_id`, and what of it is vector +/// `replacing`, in bytes: under the latest snapshot, which sees what is +/// committed now (whatever the isolation level) and this transaction's own +/// earlier writes. +fn stored( + graph_id: i32, + role: pg_sys::Oid, + replacing: Option, +) -> Result<(u64, u64), QuotaError> { + let sums = as_catalog_owner(|| { + Ok::<_, QuotaError>(read_one_latest::>( + STORED, + &[graph_id.into(), role.into(), replacing.into()], + )?) + })? + .unwrap_or_default(); + let bytes = |at: usize| sums.get(at).map_or(0, |&sum| u64::try_from(sum).unwrap_or_default()); + Ok((bytes(0), bytes(1))) +} + +/// Takes the transaction-scoped lock on (graph `graph_id`, `role`), +/// waiting for a transaction that holds it to end. Only on the leader: +/// persisting writers are `PARALLEL UNSAFE`, and a lock a parallel worker +/// took would be released when the worker exits (DESIGN.md §5.7, Spike +/// results). +fn lock(graph_id: i32, role: pg_sys::Oid) { + // SAFETY: reads the backend's database id, on the main thread. + let database = unsafe { pg_sys::MyDatabaseId }; + let tag = pg_sys::LOCKTAG { + locktag_field1: database.to_u32(), + locktag_field2: graph_id.cast_unsigned(), + locktag_field3: role.to_u32(), + locktag_field4: QUOTA_LOCK_OBJSUBID, + locktag_type: LOCKTAG_ADVISORY, + locktag_lockmethodid: USER_LOCKMETHOD, + }; + // SAFETY: a heavyweight lock request on the main thread, held until + // the transaction ends (not a session lock); waiting, it raises an + // ERROR on deadlock, `lock_timeout` or cancel, which pgrx's guarded + // FFI unwinds. With `dontWait` false it always returns having the lock. + unsafe { + pg_sys::LockAcquire(&tag, EXCLUSIVE_LOCK, false, false); + } +} + +/// `LOCKTAG_ADVISORY`, as `LOCKTAG.locktag_type` holds it. +const LOCKTAG_ADVISORY: u8 = pg_sys::LockTagType::LOCKTAG_ADVISORY as u8; + +/// `USER_LOCKMETHOD`, the lock method of advisory locks. +const USER_LOCKMETHOD: u8 = pg_sys::USER_LOCKMETHOD as u8; + +/// `ExclusiveLock`, the mode `pg_advisory_xact_lock` takes. +const EXCLUSIVE_LOCK: pg_sys::LOCKMODE = pg_sys::ExclusiveLock as pg_sys::LOCKMODE; + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +#[allow(clippy::disallowed_methods)] // Test code may assign a transaction id. +pub(super) mod tests { + use pgrx::prelude::*; + use pgrx::spi::Error; + + use super::QUOTA_LOCK_OBJSUBID; + use crate::fixture::{failure_as, refresh, ring, rows}; + + /// Graph `vq`, a ring of 100 nodes (so an `f64` vector is 800 bytes, an + /// `f32` one 400 and a `bool` one 100), with readers `vq_reader` and + /// `vq_other`, and a default quota of `default` bytes. + pub(crate) fn graph_with_readers(default: i64) -> Result<(), Error> { + ring("vq", 100, 2)?; + refresh("vq", true)?; + Spi::run(&format!( + "CREATE ROLE vq_reader; CREATE ROLE vq_other; + SELECT warren.grant('vq', 'vq_reader'); SELECT warren.grant('vq', 'vq_other'); + SELECT warren.set_vector_quota('vq', {default})" + )) + } + + /// Writes constant vector `name` of `dtype` on `vq` as `role`; the + /// SQLSTATE and message it failed with, or `None`. + pub(crate) fn write(role: &str, name: &str, dtype: &str) -> Result, Error> { + failure_as(role, &format!("warren.vector_const('vq', '{name}', 1, '{dtype}')")) + } + + /// The persisted vectors on `vq`, as `name:created_by:dtype`. + pub(crate) fn stored() -> Result, Error> { + rows( + "SELECT name || ':' || created_by::text || ':' || dtype FROM warren.vectors + ORDER BY name", + ) + } + + /// `warren.vector_usage('vq')` as `role`, as `role:vectors:bytes:quota`. + pub(crate) fn usage(role: &str) -> Result, Error> { + Spi::run(&format!("SET ROLE {role}"))?; + let found = rows( + "SELECT role::text || ':' || vectors || ':' || bytes || ':' || coalesce(quota::text, '-') + FROM warren.vector_usage('vq')", + ); + Spi::run("RESET ROLE")?; + found + } + + #[pg_test] + fn a_write_past_the_quota_is_refused_and_changes_nothing() -> Result<(), Error> { + graph_with_readers(1000)?; + assert_eq!(write("vq_reader", "a", "f64")?, None); + assert_eq!( + write("vq_reader", "b", "f32")?.as_deref(), + Some( + "53400: vector \"b\" (400 bytes) would take the persisted vectors of role \ + \"vq_reader\" on graph \"vq\" to 1200 bytes, past its quota of 1000 bytes (it \ + stores 800); drop some of its vectors, write it with persist := false, or ask \ + the graph's owner for a larger quota" + ) + ); + assert_eq!(stored()?.as_deref(), Some("a:vq_reader:f64")); + // Another role's vectors are not counted against it, nor transient ones. + assert_eq!(write("vq_other", "c", "f64")?, None); + let transient = "warren.vector_const('vq', 't', 1, persist := false)"; + assert_eq!(failure_as("vq_reader", transient)?, None); + assert_eq!(write("vq_reader", "b", "bool")?, None); + assert_eq!(usage("vq_reader")?.as_deref(), Some("vq_reader:2:900:1000")); + Ok(()) + } + + /// The transaction's own earlier writes count: a pg_test is one + /// transaction, and so is a library call. + #[pg_test] + fn writes_earlier_in_the_transaction_are_counted() -> Result<(), Error> { + graph_with_readers(1000)?; + Spi::run("SET ROLE vq_reader; SELECT warren.vector_const('vq', 'a', 1, 'f32')")?; + Spi::run("SELECT warren.vector_const('vq', 'b', 1, 'f32'); RESET ROLE")?; + assert_eq!( + write("vq_reader", "c", "f32")?.map(|error| error[..5].to_owned()).as_deref(), + Some("53400") + ); + // A vector it dropped no longer counts. + Spi::run("SET ROLE vq_reader; SELECT warren.drop_vector('vq', 'a'); RESET ROLE")?; + assert_eq!(write("vq_reader", "c", "f32")?, None); + // Nor do those an earlier write of the same call made: the second + // eigenvector (f64, 800 bytes) does not fit beside the first. + Spi::run("SELECT warren.set_vector_quota('vq', 2000)")?; + let eigen = "count(*) FROM warren.eigen('vq', 2, 'laplacian', vectors_prefix := 'e', \ + dtype := 'f64')"; + let refused = failure_as("vq_reader", eigen)?; + assert_eq!(refused.map(|error| error[..5].to_owned()).as_deref(), Some("53400")); + Spi::run("SELECT warren.set_vector_quota('vq', 2400)")?; + assert_eq!(failure_as("vq_reader", eigen)?, None); + Ok(()) + } + + #[pg_test] + fn replacing_a_vector_with_one_no_larger_always_works() -> Result<(), Error> { + graph_with_readers(1000)?; + assert_eq!(write("vq_reader", "a", "f64")?, None); + Spi::run("SELECT warren.set_vector_quota('vq', 100)")?; + assert_eq!(write("vq_reader", "a", "f64")?, None, "as large"); + assert_eq!(write("vq_reader", "a", "f32")?, None, "smaller"); + let larger = write("vq_reader", "a", "f64")?; + assert_eq!(larger.map(|error| error[..5].to_owned()).as_deref(), Some("53400")); + assert_eq!(stored()?.as_deref(), Some("a:vq_reader:f32")); + Ok(()) + } + + /// A vector the owner replaced is the owner's, and no longer counts + /// against the reader that wrote it before. + #[pg_test] + fn a_vector_counts_against_the_role_that_wrote_it() -> Result<(), Error> { + graph_with_readers(900)?; + assert_eq!(write("vq_reader", "b", "f64")?, None); + assert_eq!(write("vq_owner", "b", "f64")?, None); + assert_eq!(write("vq_reader", "c", "f64")?, None); + assert_eq!(stored()?.as_deref(), Some("b:vq_owner:f64 c:vq_reader:f64")); + assert_eq!(usage("vq_reader")?.as_deref(), Some("vq_reader:1:800:900")); + Ok(()) + } + + #[pg_test] + fn a_quota_lowered_below_use_blocks_only_growth() -> Result<(), Error> { + graph_with_readers(2000)?; + assert_eq!(write("vq_reader", "a", "f64")?, None); + assert_eq!(write("vq_reader", "b", "f64")?, None); + Spi::run("SELECT warren.set_vector_quota('vq', 500)")?; + assert_eq!(stored()?.as_deref(), Some("a:vq_reader:f64 b:vq_reader:f64"), "kept"); + let growth = write("vq_reader", "c", "bool")?; + assert_eq!(growth.map(|error| error[..5].to_owned()).as_deref(), Some("53400")); + assert_eq!(write("vq_reader", "b", "f32")?, None, "shrinking"); + assert_eq!(usage("vq_reader")?.as_deref(), Some("vq_reader:2:1200:500")); + Ok(()) + } + + #[pg_test] + fn members_of_the_owner_role_are_exempt() -> Result<(), Error> { + graph_with_readers(0)?; + Spi::run("CREATE ROLE vq_member; GRANT vq_owner TO vq_member")?; + assert_eq!(write("vq_owner", "a", "f64")?, None); + assert_eq!(write("vq_member", "b", "f64")?, None); + let refused = write("vq_reader", "c", "bool")?; + assert_eq!(refused.map(|error| error[..5].to_owned()).as_deref(), Some("53400")); + assert_eq!( + usage("vq_owner")?.as_deref(), + Some("vq_member:1:800:- vq_owner:1:800:-"), + "exempt roles have no quota" + ); + Ok(()) + } + + /// The (graph, role) lock is an advisory-type lock with pg_warren's own + /// `objsubid`, held until the transaction ends, which + /// `pg_advisory_unlock_all` does not release. + #[pg_test] + fn a_counted_write_holds_the_graph_and_role_lock() -> Result<(), Error> { + graph_with_readers(1000)?; + let held = format!( + "SELECT count(*) FROM pg_locks + WHERE locktype = 'advisory' AND objsubid = {QUOTA_LOCK_OBJSUBID} + AND pid = pg_backend_pid() AND granted + AND classid = (SELECT graph_id FROM warren.graphs WHERE name = 'vq')::oid + AND objid = 'vq_reader'::regrole::oid" + ); + assert_eq!(Spi::get_one::(&held)?, Some(0)); + assert_eq!(write("vq_reader", "a", "f64")?, None); + Spi::run("SELECT pg_advisory_unlock_all()")?; + assert_eq!(Spi::get_one::(&held)?, Some(1)); + Ok(()) + } + + /// Bytes as `compute_sql::vector_bytes` counts them, per element type + /// and width. + #[pg_test] + fn usage_counts_each_element_types_bytes() -> Result<(), Error> { + graph_with_readers(100_000)?; + Spi::run( + "SET ROLE vq_reader; + SELECT warren.vector_const('vq', 'f', 1, 'f64'); + SELECT warren.vector_const('vq', 'g', 1, 'f32'); + SELECT warren.vector_const('vq', 'i', 1, 'i64'); + SELECT warren.vector_const('vq', 'b', 1, 'bool'); + RESET ROLE", + )?; + assert_eq!(usage("vq_reader")?.as_deref(), Some("vq_reader:4:2100:100000")); + Ok(()) + } +} diff --git a/crates/pg_warren/src/vector/quota/error.rs b/crates/pg_warren/src/vector/quota/error.rs new file mode 100644 index 00000000..6201dc96 --- /dev/null +++ b/crates/pg_warren/src/vector/quota/error.rs @@ -0,0 +1,64 @@ +//! The quota feature's error type. + +use pgrx::PgSqlErrorCode; +use thiserror::Error; + +use crate::access::{AccessError, SqlError}; +use crate::catalog::CatalogError; + +/// Why a quota could not be set or read, or a write was refused. +#[derive(Debug, Error)] +pub(crate) enum QuotaError { + /// The write would take the role's persisted vectors on the graph past + /// its quota, and above what they were (DESIGN.md §14 item 47). + #[error( + "vector \"{vector}\" ({written} bytes) would take the persisted vectors of role \ + \"{role}\" on graph \"{graph}\" to {after} bytes, past its quota of {quota} bytes (it \ + stores {before}); drop some of its vectors, write it with persist := false, or ask the \ + graph's owner for a larger quota" + )] + Exceeded { + /// The vector written. + vector: String, + /// Its bytes. + written: u64, + /// The writing role. + role: String, + /// The graph. + graph: String, + /// The role's quota, in bytes. + quota: u64, + /// What it stores before the write. + before: u64, + /// What it would store after it. + after: u64, + }, + /// A quota is a number of bytes, not negative. + #[error("a vector quota cannot be negative: {0} bytes")] + Negative(i64), + /// A NULL where a value is required. + #[error("{0} must not be NULL")] + NullArgument(&'static str), + /// The graph was not found, or its row lock failed. + #[error(transparent)] + Catalog(#[from] CatalogError), + /// The caller may not do this, or an identity switch failed. + #[error(transparent)] + Access(#[from] AccessError), + /// An SPI call failed. + #[error(transparent)] + Spi(#[from] pgrx::spi::Error), +} + +impl SqlError for QuotaError { + fn sqlstate(&self) -> PgSqlErrorCode { + match self { + Self::Exceeded { .. } => PgSqlErrorCode::ERRCODE_CONFIGURATION_LIMIT_EXCEEDED, + Self::Negative(_) => PgSqlErrorCode::ERRCODE_INVALID_PARAMETER_VALUE, + Self::NullArgument(_) => PgSqlErrorCode::ERRCODE_NULL_VALUE_NOT_ALLOWED, + Self::Catalog(error) => error.sqlstate(), + Self::Access(error) => error.sqlstate(), + Self::Spi(_) => PgSqlErrorCode::ERRCODE_INTERNAL_ERROR, + } + } +} diff --git a/crates/pg_warren/src/vector/quota/mod.rs b/crates/pg_warren/src/vector/quota/mod.rs new file mode 100644 index 00000000..964df2de --- /dev/null +++ b/crates/pg_warren/src/vector/quota/mod.rs @@ -0,0 +1,94 @@ +//! Quotas on readers' vectors (DESIGN.md §5.7, §14 items 46 and 47): what +//! each role other than the graph's owner may store as persisted vectors +//! on one graph, counted as their elements' bytes +//! (`length × width × size(dtype)`) across all its versions. +//! +//! The quota is `graphs.reader_vector_quota` (the default for every role) +//! or the role's row of `warren.vector_quotas`, set by the owner with +//! `warren.set_vector_quota` and shown by `warren.vector_usage`. A +//! persisting write is admitted by [`admit`]: members of the owner role +//! are exempt; otherwise, where a quota applies, it takes the (graph, +//! role) lock, sums the role's usage under the latest committed state, and +//! refuses (53400) only a write that takes usage past the quota *and* +//! above what it was. Transient vectors, `compact`'s move, drops and +//! retention are not counted. + +/// The bytes of persisted vector `v`, a row of `warren.vectors`, in SQL: +/// what it holds when loaded, as `compute_sql::vector_bytes` counts it. +macro_rules! vector_bytes { + () => { + "v.length * v.width * CASE v.dtype WHEN 'f32' THEN 4 WHEN 'bool' THEN 1 ELSE 8 END" + }; +} + +mod enforce; +mod error; +mod sql; + +pub(crate) use enforce::admit; +pub(crate) use error::QuotaError; + +/// What a role stores on a graph before and after a write, in bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Usage { + /// Before the write. + pub(crate) before: u64, + /// After it. + pub(crate) after: u64, +} + +impl Usage { + /// Usage after writing `written` bytes in place of `replaced` bytes of + /// the role's own (0 if the write replaces none), from `stored` bytes. + pub(crate) fn of(stored: u64, replaced: u64, written: u64) -> Self { + Self { before: stored, after: stored.saturating_sub(replaced).saturating_add(written) } + } +} + +/// Whether a write of `usage` is admitted under `quota` (bytes; `None` +/// for none), the writer `exempt` if a member of the owner role: unless it +/// takes usage past the quota and above what it was before, so replacing a +/// vector with one no larger always is, and a quota lowered below what is +/// in use blocks only growth. +pub(crate) fn admits(exempt: bool, quota: Option, usage: Usage) -> bool { + exempt || quota.is_none_or(|quota| usage.after <= quota || usage.after <= usage.before) +} + +#[cfg(test)] +mod decision { + use super::{admits, Usage}; + + #[test] + fn a_write_within_the_quota_is_admitted() { + assert!(admits(false, Some(1000), Usage::of(200, 0, 800))); + assert!(!admits(false, Some(1000), Usage::of(200, 0, 801))); + } + + #[test] + fn without_a_quota_or_when_exempt_every_write_is_admitted() { + assert!(admits(false, None, Usage::of(u64::MAX, 0, u64::MAX))); + assert!(admits(true, Some(0), Usage::of(0, 0, 1))); + } + + #[test] + fn replacing_a_vector_with_one_no_larger_is_admitted_past_the_quota() { + assert!(admits(false, Some(100), Usage::of(800, 800, 800))); + assert!(admits(false, Some(100), Usage::of(800, 800, 400))); + assert!(!admits(false, Some(100), Usage::of(800, 400, 800))); + } + + #[test] + fn a_quota_lowered_below_use_blocks_only_growth() { + assert!(!admits(false, Some(500), Usage::of(800, 0, 1))); + assert!(admits(false, Some(500), Usage::of(800, 100, 100))); + assert!(!admits(false, Some(0), Usage::of(0, 0, 1))); + assert!(admits(false, Some(0), Usage::of(0, 0, 0))); + } + + #[test] + fn usage_after_a_write_replaces_only_what_it_replaces() { + assert_eq!(Usage::of(800, 300, 100), Usage { before: 800, after: 600 }); + assert_eq!(Usage::of(10, 20, 5), Usage { before: 10, after: 5 }); + assert_eq!(Usage::of(u64::MAX, 0, 1), Usage { before: u64::MAX, after: u64::MAX }); + } +} diff --git a/crates/pg_warren/src/vector/quota/sql.rs b/crates/pg_warren/src/vector/quota/sql.rs new file mode 100644 index 00000000..995554c2 --- /dev/null +++ b/crates/pg_warren/src/vector/quota/sql.rs @@ -0,0 +1,198 @@ +//! `warren.set_vector_quota` and `warren.vector_usage` (DESIGN.md §5.7, +//! §14 item 46). + +use pgrx::prelude::*; + +use super::QuotaError; +use crate::access::{as_catalog_owner, entry, is_owner, require_owner, require_reader}; +use crate::catalog::GraphDefinition; + +/// What `set_vector_quota` does, for its errors. +const ACTION: &str = "set a vector quota on"; + +/// `warren.set_vector_quota(name, bytes, role)`: sets graph `name`'s +/// quota on readers' persisted vectors to `bytes`: for `role`, or with +/// `role` NULL the default for every role. NULL `bytes` removes it: the +/// graph's default, or the role's own quota, after which the default +/// applies to the role. For members of the owner role. +#[pg_extern( + schema = "warren", + sql = r#" +CREATE FUNCTION warren."set_vector_quota"(name text, bytes bigint, role regrole DEFAULT NULL) +RETURNS void VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'set_vector_quota_wrapper'; +"# +)] +fn set_vector_quota(name: Option<&str>, bytes: Option, role: Option) { + entry(|_| set_quota(name.ok_or(QuotaError::NullArgument("name"))?, bytes, role)); +} + +fn set_quota(name: &str, bytes: Option, role: Option) -> Result<(), QuotaError> { + if let Some(negative) = bytes.filter(|&bytes| bytes < 0) { + return Err(QuotaError::Negative(negative)); + } + let mut graph = GraphDefinition::find(name)?; + require_owner(graph.owner, &graph.name, ACTION)?; + // Checks ownership again once it holds the row: a set_owner may have + // held it (DESIGN.md §5.7). + graph.lock(ACTION)?; + let statement = match (role, bytes) { + (None, _) => "UPDATE warren.graphs SET reader_vector_quota = $3 WHERE graph_id = $1", + (Some(_), None) => { + "DELETE FROM warren.vector_quotas WHERE graph_id = $1 AND role::oid = $2" + } + (Some(_), Some(_)) => { + "INSERT INTO warren.vector_quotas (graph_id, role, bytes) + VALUES ($1, $2::oid::regrole, $3) + ON CONFLICT (graph_id, role) DO UPDATE SET bytes = excluded.bytes" + } + }; + let role = role.unwrap_or(pg_sys::Oid::INVALID); + as_catalog_owner(|| { + Spi::connect_mut(|client| { + client.update(statement, None, &[graph.graph_id.into(), role.into(), bytes.into()])?; + Ok::<_, QuotaError>(()) + }) + }) +} + +/// Each role with persisted vectors or a quota of its own on graph `$1` +/// (and role `$2`, if not NULL), as (role, vectors, bytes, quota): the +/// quota that applies to it, NULL for none or for a member of the owner +/// role, which is exempt. Only role `$2`'s row if it is not NULL. +const USAGE: &str = concat!( + "SELECT r.role, + (SELECT count(*) FROM warren.vectors v + WHERE v.graph_id = $1 AND v.created_by::oid = r.role), + (SELECT coalesce(sum(", + vector_bytes!(), + "), 0)::int8 FROM warren.vectors v + WHERE v.graph_id = $1 AND v.created_by::oid = r.role), + CASE WHEN pg_catalog.pg_has_role(r.role, g.owner::oid, 'MEMBER') THEN NULL + ELSE coalesce(q.bytes, g.reader_vector_quota) END + FROM (SELECT v.created_by::oid FROM warren.vectors v WHERE v.graph_id = $1 + UNION SELECT q.role::oid FROM warren.vector_quotas q WHERE q.graph_id = $1 + UNION SELECT $2 WHERE $2 IS NOT NULL) AS r (role) + JOIN warren.graphs g ON g.graph_id = $1 + LEFT JOIN warren.vector_quotas q ON q.graph_id = $1 AND q.role::oid = r.role + WHERE $2 IS NULL OR r.role = $2 + ORDER BY r.role::regrole::text" +); + +/// `warren.vector_usage(name)`: what each role stores as persisted +/// vectors on graph `name`, and the quota that applies to it. Members of +/// the owner role see every role; a reader sees its own row. `PARALLEL +/// SAFE`: like `info`, it reads catalog rows and no vector. +#[pg_extern( + schema = "warren", + sql = r#" +CREATE FUNCTION warren."vector_usage"(name text) +RETURNS TABLE (role regrole, vectors bigint, bytes bigint, quota bigint) +STRICT STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'vector_usage_wrapper'; +"# +)] +fn vector_usage( + name: &str, +) -> TableIterator< + 'static, + (name!(role, pg_sys::Oid), name!(vectors, i64), name!(bytes, i64), name!(quota, Option)), +> { + entry(|_| Ok::<_, QuotaError>(TableIterator::new(usage(name)?))) +} + +/// A row of `vector_usage`: (role, vectors, bytes, quota). +type UsageRow = (pg_sys::Oid, i64, i64, Option); + +fn usage(name: &str) -> Result, QuotaError> { + let graph = GraphDefinition::find(name)?; + require_reader(graph.owner, &graph.readers, &graph.name, "read")?; + // SAFETY: reads the backend's current user id: the caller's role. + let only = (!is_owner(graph.owner)).then(|| unsafe { pg_sys::GetUserId() }); + as_catalog_owner(|| { + Ok::<_, QuotaError>(Spi::connect(|client| { + client + .select(USAGE, None, &[graph.graph_id.into(), only.into()])? + .map(|row| { + Ok(( + row.get::(1)?.unwrap_or(pg_sys::Oid::INVALID), + row.get::(2)?.unwrap_or_default(), + row.get::(3)?.unwrap_or_default(), + row.get::(4)?, + )) + }) + .collect::, pgrx::spi::Error>>() + })?) + }) +} + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +#[allow(clippy::disallowed_methods)] // Test code may assign a transaction id. +mod tests { + use pgrx::prelude::*; + use pgrx::spi::Error; + + use super::super::enforce::tests::{graph_with_readers, usage, write}; + use crate::fixture::{failure_as, rows}; + + #[pg_test] + fn a_roles_own_quota_overrides_the_default_until_removed() -> Result<(), Error> { + graph_with_readers(500)?; + Spi::run("SELECT warren.set_vector_quota('vq', 2000, 'vq_reader')")?; + assert_eq!(write("vq_reader", "a", "f64")?, None); + assert_eq!(write("vq_reader", "b", "f64")?, None); + let other = write("vq_other", "c", "f64")?; + assert_eq!(other.map(|error| error[..5].to_owned()).as_deref(), Some("53400")); + // vq_other has neither vectors nor a quota of its own. + assert_eq!(usage("vq_owner")?.as_deref(), Some("vq_reader:2:1600:2000")); + Spi::run( + "SELECT warren.set_vector_quota('vq', NULL, 'vq_reader'); + SELECT warren.set_vector_quota('vq', NULL)", + )?; + assert_eq!(usage("vq_owner")?.as_deref(), Some("vq_reader:2:1600:-")); + assert_eq!(write("vq_other", "c", "f64")?, None, "no quota left"); + Ok(()) + } + + #[pg_test] + fn a_reader_sees_only_its_own_usage() -> Result<(), Error> { + graph_with_readers(1000)?; + assert_eq!(write("vq_other", "c", "f64")?, None); + assert_eq!(usage("vq_reader")?.as_deref(), Some("vq_reader:0:0:1000")); + assert_eq!(usage("vq_owner")?.as_deref(), Some("vq_other:1:800:1000")); + Spi::run("CREATE ROLE vq_stranger")?; + assert_eq!( + failure_as("vq_stranger", "warren.vector_usage('vq')")?.as_deref(), + Some( + "42501: must be a member of role \"vq_owner\" or of a reader role to read graph \ + \"vq\"" + ) + ); + Ok(()) + } + + #[pg_test] + fn only_the_owner_may_set_a_quota_and_never_a_negative_one() -> Result<(), Error> { + graph_with_readers(1000)?; + assert_eq!( + failure_as("vq_reader", "warren.set_vector_quota('vq', 1, 'vq_reader')")?.as_deref(), + Some("42501: must be a member of role \"vq_owner\" to set a vector quota on graph \"vq\"") + ); + assert_eq!( + failure_as("vq_owner", "warren.set_vector_quota('vq', -1)")?.as_deref(), + Some("22023: a vector quota cannot be negative: -1 bytes") + ); + assert_eq!( + failure_as("vq_owner", "warren.set_vector_quota(NULL, 1)")?.as_deref(), + Some("22004: name must not be NULL") + ); + assert_eq!( + failure_as("vq_owner", "warren.set_vector_quota('nothing', 1)")?.as_deref(), + Some("42704: graph \"nothing\" does not exist") + ); + assert_eq!(failure_as("vq_owner", "warren.set_vector_quota('vq', 1, 'vq_reader')")?, None); + assert_eq!(failure_as("vq_owner", "warren.set_vector_quota('vq', 2, 'vq_reader')")?, None); + let rows = rows("SELECT role::text || ':' || bytes FROM warren.vector_quotas")?; + assert_eq!(rows.as_deref(), Some("vq_reader:2")); + Ok(()) + } +} diff --git a/sql/pg_warren--0.5.0--0.6.0.sql b/sql/pg_warren--0.5.0--0.6.0.sql index 3fec30d4..e57c1b2c 100644 --- a/sql/pg_warren--0.5.0--0.6.0.sql +++ b/sql/pg_warren--0.5.0--0.6.0.sql @@ -37,3 +37,27 @@ CREATE FUNCTION warren."drop_orphans"() RETURNS bigint STRICT LANGUAGE c AS 'MODULE_PATHNAME', 'drop_orphans_wrapper'; + +-- Quotas on readers' vectors (DESIGN.md §5.7, §14 items 46 and 47): the +-- graph's default, per-role overrides (dumped with the extension, revoked +-- from PUBLIC as every catalog table is), and the index a quota's sum +-- scans. Existing graphs have no quota. +ALTER TABLE warren.graphs ADD COLUMN reader_vector_quota bigint CHECK (reader_vector_quota >= 0); + +CREATE INDEX vectors_created_by ON warren.vectors (graph_id, created_by); + +CREATE TABLE warren.vector_quotas ( + graph_id int NOT NULL REFERENCES warren.graphs ON DELETE CASCADE, + role regrole NOT NULL, + bytes bigint NOT NULL CHECK (bytes >= 0), + PRIMARY KEY (graph_id, role) +); +REVOKE ALL ON warren.vector_quotas FROM PUBLIC; +SELECT pg_catalog.pg_extension_config_dump('warren.vector_quotas', ''); + +CREATE FUNCTION warren."set_vector_quota"(name text, bytes bigint, role regrole DEFAULT NULL) +RETURNS void VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'set_vector_quota_wrapper'; + +CREATE FUNCTION warren."vector_usage"(name text) +RETURNS TABLE (role regrole, vectors bigint, bytes bigint, quota bigint) +STRICT STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'vector_usage_wrapper'; diff --git a/sql/pg_warren--0.6.0.sql b/sql/pg_warren--0.6.0.sql index b5f30890..cd41123b 100644 --- a/sql/pg_warren--0.6.0.sql +++ b/sql/pg_warren--0.6.0.sql @@ -12,231 +12,82 @@ CREATE SCHEMA IF NOT EXISTS warren; /* pg_warren::extension::warren */ /* */ /* */ --- crates/pg_warren/src/community_sql/library/mod.rs:9 +-- crates/pg_warren/src/compute_sql/library/mod.rs:40 -- requires: -- extension::warren --- warren.leiden (DESIGN.md §6.7): Leiden community detection (Traag, --- Waltman and van Eck 2019) as a library over the primitives. Each level --- moves nodes between communities (warren.local_moving, from the partition --- the level below found) on the level's graph, refines each community --- into well-connected parts (warren.leiden_refine), lifts both partitions --- to `graph` (warren.lift), and contracts `graph` by the refined one --- (warren.contract) into the next level's graph; the next level starts --- from the unrefined partition, projected onto it (warren.project, min). --- As warren.louvain's, every level's graph is a quotient of `graph`, and --- only one exists at a time. Every community found is connected. As --- Traag et al.'s, it goes on while there is something to aggregate: it --- stops when a level's local moving leaves every node alone, when the --- refinement leaves every node alone (the next level's graph would be this --- one), or after `max_levels`; in the last two cases it returns the --- refined partition, so every exit returns a connected one. A level's --- local moving stops when a sweep raises modularity by no more than --- `tolerance`. That multilevel pass is one iteration; each later one --- starts its first level's moving from the partition the one before found, --- not from each node alone, as igraph's `n_iterations` and leidenalg's --- `optimise_partition` loop do. The run stops after `iterations`, or --- after an iteration whose partition is the one it started from: with the --- same start and seed, the next would find it again. A negative --- `iterations` runs until then, at most 100 iterations (a WARNING if it --- is still changing). Arguments and output as warren.louvain's; --- `randomness` is θ of the refinement. `levels_prefix` keeps every --- iteration's levels in turn (`_1`, … numbered on across --- iterations), so the last is `out`. The level's graph, a level graph of --- warren.contract's (`_warren_level_`), is dropped at the end of --- each iteration. -CREATE FUNCTION warren.leiden(graph text, "out" text, resolution float8 DEFAULT 1.0, - seed bigint DEFAULT 0, max_levels int DEFAULT 100, - tolerance float8 DEFAULT 1e-7, randomness float8 DEFAULT 0.01, - iterations int DEFAULT 2, weighted boolean DEFAULT NULL, levels_prefix text DEFAULT NULL, - version bigint DEFAULT NULL) -RETURNS float8 LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ +-- warren.clustering (DESIGN.md §6.7): the local clustering coefficient, +-- as NetworkX's clustering computes it for an undirected, unweighted +-- graph: 2T / (d(d - 1)) from each node's triangles T (warren.triangles: +-- directions and self-loops ignored) and its distinct neighbours d in +-- either direction over `rels`, and 0 where T is 0. Unlike NetworkX, a +-- self-loop counts its node among its own neighbours in d (vector_degree +-- does; no primitive tells which nodes have one). Edge weights are not +-- read. Writes vector `out` (f64). +CREATE FUNCTION warren.clustering(graph text, "out" text, rels text[] DEFAULT NULL, + version bigint DEFAULT NULL) +RETURNS void LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ DECLARE v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); - parts text[] := ARRAY['moved', 'refined', 'initial', 'keys', 'down', 'refined_down', 'prev', 'changed']; - g text := graph; - gv bigint := v; - next_g text; - next_v bigint; - level int; - iteration int := 0; - written int := 0; - nodes bigint; - found bigint; - refined bigint; - q float8; + parts text[] := ARRAY['triangles', 'degree']; BEGIN - IF "out" IS NULL OR resolution IS NULL OR seed IS NULL OR max_levels IS NULL OR tolerance IS NULL - OR randomness IS NULL OR iterations IS NULL THEN - RAISE EXCEPTION '% must not be NULL', - CASE WHEN "out" IS NULL THEN 'out' WHEN resolution IS NULL THEN 'resolution' - WHEN seed IS NULL THEN 'seed' WHEN max_levels IS NULL THEN 'max_levels' - WHEN tolerance IS NULL THEN 'tolerance' WHEN randomness IS NULL THEN 'randomness' - ELSE 'iterations' END - USING ERRCODE = 'null_value_not_allowed'; + IF "out" IS NULL THEN + RAISE EXCEPTION 'out must not be NULL' USING ERRCODE = 'null_value_not_allowed'; END IF; - IF starts_with("out", '_warren_') OR starts_with(levels_prefix, '_warren_') THEN + IF starts_with("out", '_warren_') THEN RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' USING ERRCODE = 'reserved_name'; END IF; - IF max_levels < 1 THEN - RAISE EXCEPTION 'max_levels must be at least 1, not %', max_levels USING ERRCODE = 'invalid_parameter_value'; - END IF; - IF iterations = 0 THEN - RAISE EXCEPTION 'iterations must be at least 1, or negative to run until the partition is stable, not 0' - USING ERRCODE = 'invalid_parameter_value'; - END IF; - IF tolerance = 'NaN'::float8 OR tolerance < 0 THEN - RAISE EXCEPTION 'tolerance must be a number of at least 0, not %', tolerance - USING ERRCODE = 'invalid_parameter_value'; - END IF; BEGIN - LOOP - iteration := iteration + 1; - -- A later iteration's first level moves from the last partition. - IF iteration > 1 THEN - PERFORM warren.vector_map(graph, '_warren_leiden_prev', '_warren_leiden_down', false, v); - END IF; - level := 0; - refined := NULL; - LOOP - level := level + 1; - nodes := (SELECT (e ->> 'node_count')::bigint FROM jsonb_array_elements(warren.info(g) -> 'versions') e - WHERE (e ->> 'version')::bigint = gv); - IF level > 1 THEN - PERFORM warren.vector_from_query(g, '_warren_leiden_keys', - format('SELECT node_id, node_id FROM warren.id_map(%L, NULL::bigint, %s)', g, gv), 'i64', - persist := false, version := gv); - END IF; - found := warren.local_moving(g, '_warren_leiden_moved', - initial := CASE WHEN level > 1 THEN '_warren_leiden_initial' - WHEN iteration > 1 THEN '_warren_leiden_prev' END, - keys := CASE WHEN level > 1 THEN '_warren_leiden_keys' END, - resolution := resolution, seed := seed, tolerance := tolerance, - weighted := CASE WHEN level = 1 THEN weighted ELSE true END, - persist := false, version := gv); - -- This level's partition, on `graph`. - IF level = 1 THEN - PERFORM warren.vector_map(graph, '_warren_leiden_down', '_warren_leiden_moved', false, v); - ELSE - PERFORM warren.lift(g, '_warren_leiden_moved', graph, '_warren_leiden_down', - persist := false, version := gv); - END IF; - -- Every node alone: the partition is this level's nodes, the refined - -- partition of the level below, which is connected. - IF found < nodes THEN - refined := warren.leiden_refine(g, '_warren_leiden_moved', '_warren_leiden_refined', - keys := CASE WHEN level > 1 THEN '_warren_leiden_keys' END, - resolution := resolution, seed := seed, randomness := randomness, - weighted := CASE WHEN level = 1 THEN weighted ELSE true END, - persist := false, version := gv); - IF level = 1 THEN - PERFORM warren.vector_map(graph, '_warren_leiden_refined_down', '_warren_leiden_refined', false, v); - ELSE - PERFORM warren.lift(g, '_warren_leiden_refined', graph, '_warren_leiden_refined_down', - persist := false, version := gv); - END IF; - -- Stopping here, return the refined partition, which is connected, - -- as Traag et al.'s is; the moved one need not be. - IF level >= max_levels OR refined = nodes THEN - PERFORM warren.vector_map(graph, '_warren_leiden_down', '_warren_leiden_refined_down', false, v); - END IF; - END IF; - IF levels_prefix IS NOT NULL THEN - written := written + 1; - PERFORM warren.vector_map(graph, format('%s_%s', levels_prefix, written), '_warren_leiden_down', true, v); - END IF; - EXIT WHEN found = nodes OR level >= max_levels OR refined = nodes; - -- A level graph of its own, named after its version, a name no - -- graph can have had. - next_v := warren.contract(graph, '_warren_leiden_refined_down', NULL, - weighted := coalesce(weighted, true), version := v); - next_g := '_warren_level_' || next_v; - -- The next level starts from the unrefined partition: every member - -- of a refined community is in the same community, so min is it. - PERFORM warren.project(graph, '_warren_leiden_down', next_g, '_warren_leiden_initial', 'min', - persist := false, version := next_v); - IF g <> graph THEN - PERFORM warren.drop_vector(g, '_warren_leiden_' || part, gv, true) FROM unnest(parts) part; - PERFORM warren.drop_graph(g); - END IF; - g := next_g; - gv := next_v; - END LOOP; - IF g <> graph THEN - PERFORM warren.drop_vector(g, '_warren_leiden_' || part, gv, true) FROM unnest(parts) part; - PERFORM warren.drop_graph(g); - g := graph; - gv := v; - END IF; - EXIT WHEN iteration = iterations; - -- Labels are the least dense id of each community's members, so the - -- partitions are the same exactly when the labels are. - IF iteration > 1 THEN - PERFORM warren.vector_map(graph, '_warren_leiden_changed', - 'CASE WHEN _warren_leiden_down = _warren_leiden_prev THEN 0 ELSE 1 END', false, v); - EXIT WHEN warren.vector_reduce(graph, '_warren_leiden_changed', 'sum', v) = 0; - END IF; - IF iterations < 0 AND iteration = 100 THEN - RAISE WARNING 'leiden did not stabilise in 100 iterations'; - EXIT; - END IF; - END LOOP; - PERFORM warren.vector_map(graph, "out", '_warren_leiden_down', true, v); - q := warren.modularity(graph, "out", resolution, weighted, v); + PERFORM warren.triangles(graph, '_warren_clustering_triangles', rels, false, v); + PERFORM warren.vector_degree(graph, '_warren_clustering_degree', 'both', rels, 'f64', false, v); + PERFORM warren.vector_map(graph, "out", + 'CASE WHEN _warren_clustering_triangles = 0 THEN 0 + ELSE 2 * _warren_clustering_triangles::float8 + / (_warren_clustering_degree * (_warren_clustering_degree - 1)) END', true, v); EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_leiden_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_clustering_' || part, v, true) FROM unnest(parts) part; RAISE; END; - PERFORM warren.drop_vector(graph, '_warren_leiden_' || part, v, true) FROM unnest(parts) part; - IF g <> graph THEN - PERFORM warren.drop_vector(g, '_warren_leiden_' || part, gv, true) FROM unnest(parts) part; - PERFORM warren.drop_graph(g); - END IF; - RETURN q; + PERFORM warren.drop_vector(graph, '_warren_clustering_' || part, v, true) FROM unnest(parts) part; END $$; /* */ /* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:34 +-- crates/pg_warren/src/compute_sql/library/mod.rs:29 -- requires: -- extension::warren --- warren.katz_centrality (DESIGN.md §6.7): Katz centrality by power --- iteration, as NetworkX computes it. Centrality flows along edges, --- weighted by edge weight (spmv's rule: weights if the graph has them, --- unless weighted := false), attenuated by alpha, plus each node's beta: --- the constant `beta`, or beta × vector `seed` (NetworkX's per-node beta). --- From 0, each iteration takes alpha · Aᵀx + β; it stops when the L1 change --- falls below (live nodes) × tol, or after max_iter iterations with a --- WARNING (NetworkX raises). Alpha at or above 1 / the largest eigenvalue --- diverges: once a value or the L2 norm is not finite the run is refused --- (22003), as NetworkX raises, and writes nothing. With normalized, the --- result is scaled to unit L2 norm. Writes vector `out` (f64) and returns --- the iterations run. Constants enter expressions as constant vectors; a --- constant vector is 0 at tombstoned rows, where vector_map evaluates too, --- so a division by one is guarded. -CREATE FUNCTION warren.katz_centrality(graph text, "out" text, alpha float8 DEFAULT 0.1, - beta float8 DEFAULT 1.0, seed text DEFAULT NULL, - normalized boolean DEFAULT true, tol float8 DEFAULT 1e-6, - max_iter int DEFAULT 1000, rels text[] DEFAULT NULL, - weighted boolean DEFAULT NULL, version bigint DEFAULT NULL) +-- warren.eigenvector_centrality (DESIGN.md §6.7): eigenvector centrality +-- by power iteration, as NetworkX computes it. Centrality flows along edges +-- (the left eigenvector of A: a node is central when central nodes point at +-- it), weighted by edge weight (spmv's rule: weights if the graph has them, +-- unless weighted := false). From the uniform 1/N, each iteration takes +-- x + Aᵀx (the shift by I keeps a periodic graph from oscillating) and +-- scales it to unit L2 norm; it stops when the L1 change falls below +-- (live nodes) × tol, or after max_iter iterations with a WARNING (NetworkX +-- raises). Values too large for double precision (an L2 norm past about +-- 1e154) are refused (22003) and write nothing. Writes vector `out` (f64, +-- unit L2 norm) and returns the iterations run. Constants enter +-- expressions as constant vectors; a constant vector is 0 at tombstoned +-- rows, where vector_map evaluates too, so a division by one is guarded. +CREATE FUNCTION warren.eigenvector_centrality(graph text, "out" text, tol float8 DEFAULT 1e-6, + max_iter int DEFAULT 100, rels text[] DEFAULT NULL, + weighted boolean DEFAULT NULL, version bigint DEFAULT NULL) RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ DECLARE v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); - parts text[] := ARRAY['one', 'b', 'alpha', 'x', 'y', 'next', 'scale']; + parts text[] := ARRAY['one', 'x', 'y', 'next', 'scale']; nodes float8; norm float8; delta float8; i int := 0; BEGIN - IF "out" IS NULL OR alpha IS NULL OR beta IS NULL OR normalized IS NULL OR tol IS NULL OR max_iter IS NULL THEN - RAISE EXCEPTION '% must not be NULL', - CASE WHEN "out" IS NULL THEN 'out' WHEN alpha IS NULL THEN 'alpha' WHEN beta IS NULL THEN 'beta' - WHEN normalized IS NULL THEN 'normalized' WHEN tol IS NULL THEN 'tol' ELSE 'max_iter' END + IF "out" IS NULL OR tol IS NULL OR max_iter IS NULL THEN + RAISE EXCEPTION '% must not be NULL', CASE WHEN "out" IS NULL THEN 'out' WHEN tol IS NULL THEN 'tol' ELSE 'max_iter' END USING ERRCODE = 'null_value_not_allowed'; END IF; - IF starts_with("out", '_warren_') OR starts_with(seed, '_warren_') THEN + IF starts_with("out", '_warren_') THEN RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' USING ERRCODE = 'reserved_name'; END IF; @@ -247,129 +98,203 @@ BEGIN RAISE EXCEPTION 'max_iter must be at least 1, not %', max_iter USING ERRCODE = 'invalid_parameter_value'; END IF; BEGIN - PERFORM warren.vector_const(graph, '_warren_katz_centrality_one', 1, persist := false, version := v); - nodes := warren.vector_reduce(graph, '_warren_katz_centrality_one', 'sum', v); + PERFORM warren.vector_const(graph, '_warren_eigenvector_centrality_one', 1, persist := false, version := v); + nodes := warren.vector_reduce(graph, '_warren_eigenvector_centrality_one', 'sum', v); IF nodes = 0 THEN RAISE EXCEPTION 'graph "%" has no live nodes', graph USING ERRCODE = 'invalid_parameter_value'; END IF; - IF seed IS NULL THEN - PERFORM warren.vector_const(graph, '_warren_katz_centrality_b', beta, persist := false, version := v); - ELSE - -- vector_reduce names a missing seed vector (42704); vector_map would - -- call it a missing column. - PERFORM warren.vector_reduce(graph, seed, 'nnz', v); - PERFORM warren.vector_map(graph, '_warren_katz_centrality_b', format('%I::float8', seed), false, v); - PERFORM warren.vector_const(graph, '_warren_katz_centrality_scale', beta, persist := false, version := v); - PERFORM warren.vector_map(graph, '_warren_katz_centrality_b', - '_warren_katz_centrality_scale * _warren_katz_centrality_b', false, v); - END IF; - PERFORM warren.vector_const(graph, '_warren_katz_centrality_alpha', alpha, persist := false, version := v); - PERFORM warren.vector_const(graph, '_warren_katz_centrality_x', 0, persist := false, version := v); + PERFORM warren.vector_const(graph, '_warren_eigenvector_centrality_x', 1.0 / nodes, persist := false, + version := v); WHILE i < max_iter LOOP i := i + 1; - PERFORM warren.spmv(graph, '_warren_katz_centrality_x', '_warren_katz_centrality_y', + PERFORM warren.spmv(graph, '_warren_eigenvector_centrality_x', '_warren_eigenvector_centrality_y', transpose := true, rels := rels, weighted := weighted, persist := false, version := v); - PERFORM warren.vector_map(graph, '_warren_katz_centrality_next', - '_warren_katz_centrality_alpha * _warren_katz_centrality_y + _warren_katz_centrality_b', false, v); - delta := warren.vector_norm_diff(graph, '_warren_katz_centrality_next', '_warren_katz_centrality_x', - 'l1', v, v); - IF NOT delta < 'Infinity' THEN - RAISE EXCEPTION 'katz_centrality diverged: its values are not finite after % iterations', i + PERFORM warren.vector_map(graph, '_warren_eigenvector_centrality_next', + '_warren_eigenvector_centrality_x + _warren_eigenvector_centrality_y', false, v); + norm := warren.vector_reduce(graph, '_warren_eigenvector_centrality_next', 'l2', v); + IF NOT norm < 'Infinity' THEN + RAISE EXCEPTION 'eigenvector_centrality diverged: its values are not finite after % iterations', i USING ERRCODE = 'numeric_value_out_of_range', - HINT = 'alpha must be below 1 / the largest eigenvalue of the adjacency matrix.'; + HINT = 'The edge weights are too large: their squares overflow double precision.'; END IF; - PERFORM warren.vector_map(graph, '_warren_katz_centrality_x', '_warren_katz_centrality_next', false, v); + PERFORM warren.vector_const(graph, '_warren_eigenvector_centrality_scale', + CASE WHEN norm = 0 THEN 1 ELSE norm END, persist := false, version := v); + PERFORM warren.vector_map(graph, '_warren_eigenvector_centrality_next', + 'CASE WHEN _warren_eigenvector_centrality_scale <> 0 + THEN _warren_eigenvector_centrality_next / _warren_eigenvector_centrality_scale ELSE 0 END', false, v); + delta := warren.vector_norm_diff(graph, '_warren_eigenvector_centrality_next', + '_warren_eigenvector_centrality_x', 'l1', v, v); + PERFORM warren.vector_map(graph, '_warren_eigenvector_centrality_x', '_warren_eigenvector_centrality_next', + false, v); EXIT WHEN delta < nodes * tol; END LOOP; IF NOT delta < nodes * tol THEN - RAISE WARNING 'katz_centrality did not converge in % iterations', i; - END IF; - IF normalized THEN - norm := warren.vector_reduce(graph, '_warren_katz_centrality_x', 'l2', v); - IF NOT norm < 'Infinity' THEN - RAISE EXCEPTION 'katz_centrality diverged: its L2 norm overflows after % iterations', i - USING ERRCODE = 'numeric_value_out_of_range', - HINT = 'alpha must be below 1 / the largest eigenvalue of the adjacency matrix.'; - END IF; - PERFORM warren.vector_const(graph, '_warren_katz_centrality_scale', CASE WHEN norm = 0 THEN 1 ELSE norm END, - persist := false, version := v); - PERFORM warren.vector_map(graph, "out", - 'CASE WHEN _warren_katz_centrality_scale <> 0 - THEN _warren_katz_centrality_x / _warren_katz_centrality_scale ELSE 0 END', true, v); - ELSE - PERFORM warren.vector_map(graph, "out", '_warren_katz_centrality_x', true, v); + RAISE WARNING 'eigenvector_centrality did not converge in % iterations', i; END IF; + PERFORM warren.vector_map(graph, "out", '_warren_eigenvector_centrality_x', true, v); EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_katz_centrality_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_eigenvector_centrality_' || part, v, true) FROM unnest(parts) part; RAISE; END; - PERFORM warren.drop_vector(graph, '_warren_katz_centrality_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_eigenvector_centrality_' || part, v, true) FROM unnest(parts) part; RETURN i; END $$; /* */ /* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:18 +-- crates/pg_warren/src/compute_sql/library/mod.rs:7 -- requires: -- extension::warren --- warren.label_propagation (DESIGN.md §6.7, A.1): min-label propagation. --- Labels start as vector `seed` (default: each node's dense id); each --- iteration every node takes the smallest label among itself and its --- neighbours (both directions, over `rels`), until no label changes or --- after max_iter iterations. With `mask`, only the nodes it selects (a --- non-zero value, GraphBLAS semantics) send or take labels; the others --- keep theirs. It is deterministic min-propagation, not majority-vote --- label propagation. Writes vector `out` (f64) and returns the iterations --- run. -CREATE FUNCTION warren.label_propagation(graph text, "out" text, seed text DEFAULT NULL, - max_iter int DEFAULT 100, mask text DEFAULT NULL, - rels text[] DEFAULT NULL, version bigint DEFAULT NULL) +-- warren.pagerank (DESIGN.md §6.7): PageRank by power iteration, as +-- NetworkX computes it. Rank flows along edges, split by out-weight (the +-- edge count when unweighted); dangling nodes' rank, and the 1 - damping +-- teleport, go to the personalisation: uniform, or proportional to vector +-- `seed`. Stops when the L1 change falls below (live nodes) × tol, or after +-- max_iter iterations. Writes vector `out` (f64, summing to 1) and returns +-- the iterations run. Constants enter expressions as constant vectors, +-- never as text, so no client setting (extra_float_digits) rounds them. +CREATE FUNCTION warren.pagerank(graph text, "out" text, damping float8 DEFAULT 0.85, + tol float8 DEFAULT 1e-6, max_iter int DEFAULT 100, + seed text DEFAULT NULL, rels text[] DEFAULT NULL, + weighted boolean DEFAULT NULL, version bigint DEFAULT NULL) RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ DECLARE v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); - parts text[] := ARRAY['x', 'msg', 'next']; - selected text; + parts text[] := ARRAY['one', 'out', 'x', 'p', 'total', 'damping', 'teleport', 'msg', 'dangling_x', + 'dangling', 'y', 'next']; + nodes float8; + dangling float8; + delta float8; + i int := 0; +BEGIN + IF starts_with("out", '_warren_') OR starts_with(seed, '_warren_') THEN + RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' + USING ERRCODE = 'reserved_name'; + END IF; + BEGIN + -- Every live node's out-weight: the row sums of A. + PERFORM warren.vector_const(graph, '_warren_pagerank_one', 1, persist := false, version := v); + PERFORM warren.spmv(graph, '_warren_pagerank_one', '_warren_pagerank_out', 'plus_first', + rels := rels, weighted := weighted, persist := false, version := v); + nodes := warren.vector_reduce(graph, '_warren_pagerank_one', 'sum', v); + PERFORM warren.vector_const(graph, '_warren_pagerank_x', 1.0 / nodes, persist := false, version := v); + IF seed IS NULL THEN + PERFORM warren.vector_map(graph, '_warren_pagerank_p', '_warren_pagerank_x', false, v); + ELSE + -- Times the reciprocal: a constant vector is 0 at tombstoned rows, + -- where vector_map evaluates too, so dividing by one would fail. + PERFORM warren.vector_const(graph, '_warren_pagerank_total', 1.0 / warren.vector_reduce(graph, seed, 'sum', v), + persist := false, version := v); + PERFORM warren.vector_map(graph, '_warren_pagerank_p', format('%I * _warren_pagerank_total', seed), false, v); + END IF; + PERFORM warren.vector_const(graph, '_warren_pagerank_damping', damping, persist := false, version := v); + PERFORM warren.vector_const(graph, '_warren_pagerank_teleport', 1 - damping, persist := false, version := v); + WHILE i < max_iter LOOP + i := i + 1; + PERFORM warren.vector_map(graph, '_warren_pagerank_msg', + 'CASE WHEN _warren_pagerank_out > 0 THEN _warren_pagerank_x / _warren_pagerank_out ELSE 0 END', false, v); + PERFORM warren.vector_map(graph, '_warren_pagerank_dangling_x', + 'CASE WHEN _warren_pagerank_out > 0 THEN 0 ELSE _warren_pagerank_x END', false, v); + dangling := warren.vector_reduce(graph, '_warren_pagerank_dangling_x', 'sum', v); + PERFORM warren.vector_const(graph, '_warren_pagerank_dangling', dangling, persist := false, version := v); + PERFORM warren.spmv(graph, '_warren_pagerank_msg', '_warren_pagerank_y', transpose := true, + rels := rels, weighted := weighted, persist := false, version := v); + PERFORM warren.vector_map(graph, '_warren_pagerank_next', + '_warren_pagerank_damping * (_warren_pagerank_y + _warren_pagerank_dangling * _warren_pagerank_p) + + _warren_pagerank_teleport * _warren_pagerank_p', false, v); + delta := warren.vector_norm_diff(graph, '_warren_pagerank_next', '_warren_pagerank_x', 'l1', v, v); + PERFORM warren.vector_map(graph, '_warren_pagerank_x', '_warren_pagerank_next', false, v); + EXIT WHEN delta < nodes * tol; + END LOOP; + PERFORM warren.vector_map(graph, "out", '_warren_pagerank_x', true, v); + EXCEPTION WHEN OTHERS OR query_canceled THEN + PERFORM warren.drop_vector(graph, '_warren_pagerank_' || part, v, true) FROM unnest(parts) part; + RAISE; + END; + PERFORM warren.drop_vector(graph, '_warren_pagerank_' || part, v, true) FROM unnest(parts) part; + RETURN i; +END $$; +/* */ + +/* */ +-- crates/pg_warren/src/compute_sql/library/mod.rs:41 +-- requires: +-- extension::warren + +-- warren.label_propagation_majority (DESIGN.md §6.7): majority-vote label +-- propagation, synchronous and deterministic. Labels start as vector `seed` +-- (default: each node's dense id); each iteration every node takes the most +-- frequent label among its own and its neighbours' (direction ignored, each +-- neighbour once, over `rels`; a self-loop counts the node's own label once +-- more; weights are not read), the smallest on a tie, until no label +-- changes, or after max_iter iterations with a WARNING (a synchronous +-- update can oscillate). NetworkX's label propagation is asynchronous +-- (random order and ties) or semi-synchronous (by colouring), so neither +-- matches step for step. Tier 2: each iteration is one `gather` (mode, with +-- include_self), which counts the labels in-engine, N rows through the +-- executor. Writes vector `out` (f64) and returns the iterations run. +CREATE FUNCTION warren.label_propagation_majority(graph text, "out" text, seed text DEFAULT NULL, + max_iter int DEFAULT 100, rels text[] DEFAULT NULL, + version bigint DEFAULT NULL) +RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ +DECLARE + info jsonb := warren.info(graph); + v bigint := coalesce(version, (info ->> 'current_version')::bigint); + parts text[] := ARRAY['x', 'next']; + -- Each node's own label and its neighbours', counted in-engine; the most + -- votes win, then the smallest label. + votes text := format( + 'SELECT node_id, value + FROM warren.gather(%1$L, %2$L, NULL::%3$s, NULL::float8, ''mode'', ''both'', %4$L::text[], + version := %5$s, include_self := true)', + graph, '_warren_label_propagation_majority_x', info ->> 'id_type', rels, v); + nodes bigint; delta float8; i int := 0; BEGIN - IF starts_with("out", '_warren_') OR starts_with(seed, '_warren_') OR starts_with(mask, '_warren_') THEN + IF "out" IS NULL OR max_iter IS NULL THEN + RAISE EXCEPTION '% must not be NULL', CASE WHEN "out" IS NULL THEN 'out' ELSE 'max_iter' END + USING ERRCODE = 'null_value_not_allowed'; + END IF; + IF starts_with("out", '_warren_') OR starts_with(seed, '_warren_') THEN RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' USING ERRCODE = 'reserved_name'; END IF; + IF max_iter < 1 THEN + RAISE EXCEPTION 'max_iter must be at least 1, not %', max_iter USING ERRCODE = 'invalid_parameter_value'; + END IF; BEGIN IF seed IS NULL THEN - PERFORM warren.vector_dense_id(graph, '_warren_label_propagation_x', 'f64', false, v); + PERFORM warren.vector_dense_id(graph, '_warren_label_propagation_majority_x', 'f64', false, v); ELSE - PERFORM warren.vector_map(graph, '_warren_label_propagation_x', format('%I::float8', seed), false, v); - END IF; - IF mask IS NOT NULL THEN - selected := CASE warren.explain_map(graph, format('%I', mask), v) ->> 'dtype' - WHEN 'bool' THEN format('%I', mask) ELSE format('%I <> 0', mask) END; + -- vector_reduce names a missing seed vector (42704); vector_map would + -- call it a missing column. + PERFORM warren.vector_reduce(graph, seed, 'nnz', v); + PERFORM warren.vector_map(graph, '_warren_label_propagation_majority_x', format('%I::float8', seed), false, v); END IF; - WHILE i < max_iter LOOP + -- No live nodes, no labels: no iterations (NetworkX finds no communities). + nodes := (warren.vector_stats(graph, '_warren_label_propagation_majority_x', v) ->> 'count')::bigint; + WHILE i < max_iter AND nodes > 0 LOOP i := i + 1; - -- Unselected nodes send NULL (NaN), which min never picks over a label. - PERFORM warren.vector_map(graph, '_warren_label_propagation_msg', - CASE WHEN selected IS NULL THEN '_warren_label_propagation_x' - ELSE format('CASE WHEN %s THEN _warren_label_propagation_x END', selected) END, false, v); - PERFORM warren.vector_map(graph, '_warren_label_propagation_next', '_warren_label_propagation_x', false, v); - PERFORM warren.spmv(graph, '_warren_label_propagation_msg', '_warren_label_propagation_next', 'min_second', - rels := rels, mask := mask, accumulate := 'min', persist := false, version := v); - PERFORM warren.spmv(graph, '_warren_label_propagation_msg', '_warren_label_propagation_next', 'min_second', - transpose := true, rels := rels, mask := mask, accumulate := 'min', persist := false, - version := v); - delta := warren.vector_norm_diff(graph, '_warren_label_propagation_next', '_warren_label_propagation_x', - 'l1', v, v); - PERFORM warren.vector_map(graph, '_warren_label_propagation_x', '_warren_label_propagation_next', false, v); + PERFORM warren.vector_from_query(graph, '_warren_label_propagation_majority_next', votes, 'f64', + persist := false, version := v); + delta := warren.vector_norm_diff(graph, '_warren_label_propagation_majority_next', + '_warren_label_propagation_majority_x', 'l1', v, v); + PERFORM warren.vector_map(graph, '_warren_label_propagation_majority_x', + '_warren_label_propagation_majority_next', false, v); EXIT WHEN delta = 0; END LOOP; - PERFORM warren.vector_map(graph, "out", '_warren_label_propagation_x', true, v); + IF delta <> 0 THEN + RAISE WARNING 'label_propagation_majority did not converge in % iterations', i; + END IF; + PERFORM warren.vector_map(graph, "out", '_warren_label_propagation_majority_x', true, v); EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_label_propagation_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_label_propagation_majority_' || part, v, true) + FROM unnest(parts) part; RAISE; END; - PERFORM warren.drop_vector(graph, '_warren_label_propagation_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_label_propagation_majority_' || part, v, true) FROM unnest(parts) part; RETURN i; END $$; /* */ @@ -483,139 +408,174 @@ END $$; /* */ /* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:40 --- requires: --- extension::warren - --- warren.clustering (DESIGN.md §6.7): the local clustering coefficient, --- as NetworkX's clustering computes it for an undirected, unweighted --- graph: 2T / (d(d - 1)) from each node's triangles T (warren.triangles: --- directions and self-loops ignored) and its distinct neighbours d in --- either direction over `rels`, and 0 where T is 0. Unlike NetworkX, a --- self-loop counts its node among its own neighbours in d (vector_degree --- does; no primitive tells which nodes have one). Edge weights are not --- read. Writes vector `out` (f64). -CREATE FUNCTION warren.clustering(graph text, "out" text, rels text[] DEFAULT NULL, - version bigint DEFAULT NULL) -RETURNS void LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ -DECLARE - v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); - parts text[] := ARRAY['triangles', 'degree']; -BEGIN - IF "out" IS NULL THEN - RAISE EXCEPTION 'out must not be NULL' USING ERRCODE = 'null_value_not_allowed'; - END IF; - IF starts_with("out", '_warren_') THEN - RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' - USING ERRCODE = 'reserved_name'; - END IF; - BEGIN - PERFORM warren.triangles(graph, '_warren_clustering_triangles', rels, false, v); - PERFORM warren.vector_degree(graph, '_warren_clustering_degree', 'both', rels, 'f64', false, v); - PERFORM warren.vector_map(graph, "out", - 'CASE WHEN _warren_clustering_triangles = 0 THEN 0 - ELSE 2 * _warren_clustering_triangles::float8 - / (_warren_clustering_degree * (_warren_clustering_degree - 1)) END', true, v); - EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_clustering_' || part, v, true) FROM unnest(parts) part; - RAISE; - END; - PERFORM warren.drop_vector(graph, '_warren_clustering_' || part, v, true) FROM unnest(parts) part; -END $$; -/* */ - -/* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:7 +-- crates/pg_warren/src/compute_sql/library/mod.rs:34 -- requires: -- extension::warren --- warren.pagerank (DESIGN.md §6.7): PageRank by power iteration, as --- NetworkX computes it. Rank flows along edges, split by out-weight (the --- edge count when unweighted); dangling nodes' rank, and the 1 - damping --- teleport, go to the personalisation: uniform, or proportional to vector --- `seed`. Stops when the L1 change falls below (live nodes) × tol, or after --- max_iter iterations. Writes vector `out` (f64, summing to 1) and returns --- the iterations run. Constants enter expressions as constant vectors, --- never as text, so no client setting (extra_float_digits) rounds them. -CREATE FUNCTION warren.pagerank(graph text, "out" text, damping float8 DEFAULT 0.85, - tol float8 DEFAULT 1e-6, max_iter int DEFAULT 100, - seed text DEFAULT NULL, rels text[] DEFAULT NULL, - weighted boolean DEFAULT NULL, version bigint DEFAULT NULL) +-- warren.katz_centrality (DESIGN.md §6.7): Katz centrality by power +-- iteration, as NetworkX computes it. Centrality flows along edges, +-- weighted by edge weight (spmv's rule: weights if the graph has them, +-- unless weighted := false), attenuated by alpha, plus each node's beta: +-- the constant `beta`, or beta × vector `seed` (NetworkX's per-node beta). +-- From 0, each iteration takes alpha · Aᵀx + β; it stops when the L1 change +-- falls below (live nodes) × tol, or after max_iter iterations with a +-- WARNING (NetworkX raises). Alpha at or above 1 / the largest eigenvalue +-- diverges: once a value or the L2 norm is not finite the run is refused +-- (22003), as NetworkX raises, and writes nothing. With normalized, the +-- result is scaled to unit L2 norm. Writes vector `out` (f64) and returns +-- the iterations run. Constants enter expressions as constant vectors; a +-- constant vector is 0 at tombstoned rows, where vector_map evaluates too, +-- so a division by one is guarded. +CREATE FUNCTION warren.katz_centrality(graph text, "out" text, alpha float8 DEFAULT 0.1, + beta float8 DEFAULT 1.0, seed text DEFAULT NULL, + normalized boolean DEFAULT true, tol float8 DEFAULT 1e-6, + max_iter int DEFAULT 1000, rels text[] DEFAULT NULL, + weighted boolean DEFAULT NULL, version bigint DEFAULT NULL) RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ DECLARE v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); - parts text[] := ARRAY['one', 'out', 'x', 'p', 'total', 'damping', 'teleport', 'msg', 'dangling_x', - 'dangling', 'y', 'next']; + parts text[] := ARRAY['one', 'b', 'alpha', 'x', 'y', 'next', 'scale']; nodes float8; - dangling float8; + norm float8; delta float8; i int := 0; BEGIN + IF "out" IS NULL OR alpha IS NULL OR beta IS NULL OR normalized IS NULL OR tol IS NULL OR max_iter IS NULL THEN + RAISE EXCEPTION '% must not be NULL', + CASE WHEN "out" IS NULL THEN 'out' WHEN alpha IS NULL THEN 'alpha' WHEN beta IS NULL THEN 'beta' + WHEN normalized IS NULL THEN 'normalized' WHEN tol IS NULL THEN 'tol' ELSE 'max_iter' END + USING ERRCODE = 'null_value_not_allowed'; + END IF; IF starts_with("out", '_warren_') OR starts_with(seed, '_warren_') THEN RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' USING ERRCODE = 'reserved_name'; END IF; + IF tol < 0 THEN + RAISE EXCEPTION 'tol must be at least 0, not %', tol USING ERRCODE = 'invalid_parameter_value'; + END IF; + IF max_iter < 1 THEN + RAISE EXCEPTION 'max_iter must be at least 1, not %', max_iter USING ERRCODE = 'invalid_parameter_value'; + END IF; BEGIN - -- Every live node's out-weight: the row sums of A. - PERFORM warren.vector_const(graph, '_warren_pagerank_one', 1, persist := false, version := v); - PERFORM warren.spmv(graph, '_warren_pagerank_one', '_warren_pagerank_out', 'plus_first', - rels := rels, weighted := weighted, persist := false, version := v); - nodes := warren.vector_reduce(graph, '_warren_pagerank_one', 'sum', v); - PERFORM warren.vector_const(graph, '_warren_pagerank_x', 1.0 / nodes, persist := false, version := v); + PERFORM warren.vector_const(graph, '_warren_katz_centrality_one', 1, persist := false, version := v); + nodes := warren.vector_reduce(graph, '_warren_katz_centrality_one', 'sum', v); + IF nodes = 0 THEN + RAISE EXCEPTION 'graph "%" has no live nodes', graph USING ERRCODE = 'invalid_parameter_value'; + END IF; IF seed IS NULL THEN - PERFORM warren.vector_map(graph, '_warren_pagerank_p', '_warren_pagerank_x', false, v); + PERFORM warren.vector_const(graph, '_warren_katz_centrality_b', beta, persist := false, version := v); ELSE - -- Times the reciprocal: a constant vector is 0 at tombstoned rows, - -- where vector_map evaluates too, so dividing by one would fail. - PERFORM warren.vector_const(graph, '_warren_pagerank_total', 1.0 / warren.vector_reduce(graph, seed, 'sum', v), - persist := false, version := v); - PERFORM warren.vector_map(graph, '_warren_pagerank_p', format('%I * _warren_pagerank_total', seed), false, v); + -- vector_reduce names a missing seed vector (42704); vector_map would + -- call it a missing column. + PERFORM warren.vector_reduce(graph, seed, 'nnz', v); + PERFORM warren.vector_map(graph, '_warren_katz_centrality_b', format('%I::float8', seed), false, v); + PERFORM warren.vector_const(graph, '_warren_katz_centrality_scale', beta, persist := false, version := v); + PERFORM warren.vector_map(graph, '_warren_katz_centrality_b', + '_warren_katz_centrality_scale * _warren_katz_centrality_b', false, v); END IF; - PERFORM warren.vector_const(graph, '_warren_pagerank_damping', damping, persist := false, version := v); - PERFORM warren.vector_const(graph, '_warren_pagerank_teleport', 1 - damping, persist := false, version := v); + PERFORM warren.vector_const(graph, '_warren_katz_centrality_alpha', alpha, persist := false, version := v); + PERFORM warren.vector_const(graph, '_warren_katz_centrality_x', 0, persist := false, version := v); WHILE i < max_iter LOOP i := i + 1; - PERFORM warren.vector_map(graph, '_warren_pagerank_msg', - 'CASE WHEN _warren_pagerank_out > 0 THEN _warren_pagerank_x / _warren_pagerank_out ELSE 0 END', false, v); - PERFORM warren.vector_map(graph, '_warren_pagerank_dangling_x', - 'CASE WHEN _warren_pagerank_out > 0 THEN 0 ELSE _warren_pagerank_x END', false, v); - dangling := warren.vector_reduce(graph, '_warren_pagerank_dangling_x', 'sum', v); - PERFORM warren.vector_const(graph, '_warren_pagerank_dangling', dangling, persist := false, version := v); - PERFORM warren.spmv(graph, '_warren_pagerank_msg', '_warren_pagerank_y', transpose := true, - rels := rels, weighted := weighted, persist := false, version := v); - PERFORM warren.vector_map(graph, '_warren_pagerank_next', - '_warren_pagerank_damping * (_warren_pagerank_y + _warren_pagerank_dangling * _warren_pagerank_p) - + _warren_pagerank_teleport * _warren_pagerank_p', false, v); - delta := warren.vector_norm_diff(graph, '_warren_pagerank_next', '_warren_pagerank_x', 'l1', v, v); - PERFORM warren.vector_map(graph, '_warren_pagerank_x', '_warren_pagerank_next', false, v); + PERFORM warren.spmv(graph, '_warren_katz_centrality_x', '_warren_katz_centrality_y', + transpose := true, rels := rels, weighted := weighted, persist := false, version := v); + PERFORM warren.vector_map(graph, '_warren_katz_centrality_next', + '_warren_katz_centrality_alpha * _warren_katz_centrality_y + _warren_katz_centrality_b', false, v); + delta := warren.vector_norm_diff(graph, '_warren_katz_centrality_next', '_warren_katz_centrality_x', + 'l1', v, v); + IF NOT delta < 'Infinity' THEN + RAISE EXCEPTION 'katz_centrality diverged: its values are not finite after % iterations', i + USING ERRCODE = 'numeric_value_out_of_range', + HINT = 'alpha must be below 1 / the largest eigenvalue of the adjacency matrix.'; + END IF; + PERFORM warren.vector_map(graph, '_warren_katz_centrality_x', '_warren_katz_centrality_next', false, v); EXIT WHEN delta < nodes * tol; END LOOP; - PERFORM warren.vector_map(graph, "out", '_warren_pagerank_x', true, v); + IF NOT delta < nodes * tol THEN + RAISE WARNING 'katz_centrality did not converge in % iterations', i; + END IF; + IF normalized THEN + norm := warren.vector_reduce(graph, '_warren_katz_centrality_x', 'l2', v); + IF NOT norm < 'Infinity' THEN + RAISE EXCEPTION 'katz_centrality diverged: its L2 norm overflows after % iterations', i + USING ERRCODE = 'numeric_value_out_of_range', + HINT = 'alpha must be below 1 / the largest eigenvalue of the adjacency matrix.'; + END IF; + PERFORM warren.vector_const(graph, '_warren_katz_centrality_scale', CASE WHEN norm = 0 THEN 1 ELSE norm END, + persist := false, version := v); + PERFORM warren.vector_map(graph, "out", + 'CASE WHEN _warren_katz_centrality_scale <> 0 + THEN _warren_katz_centrality_x / _warren_katz_centrality_scale ELSE 0 END', true, v); + ELSE + PERFORM warren.vector_map(graph, "out", '_warren_katz_centrality_x', true, v); + END IF; EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_pagerank_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_katz_centrality_' || part, v, true) FROM unnest(parts) part; RAISE; END; - PERFORM warren.drop_vector(graph, '_warren_pagerank_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_katz_centrality_' || part, v, true) FROM unnest(parts) part; RETURN i; END $$; /* */ /* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:8 +-- crates/pg_warren/src/compute_sql/library/mod.rs:23 -- requires: -- extension::warren --- warren.degree_vector (DESIGN.md §6.7): each node's degree in `direction` --- over `rels`, as vector `out` (f64); returns its vector id. -CREATE FUNCTION warren.degree_vector(graph text, "out" text, direction text DEFAULT 'out', - rels text[] DEFAULT NULL, version bigint DEFAULT NULL) -RETURNS bigint LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ +-- warren.sssp_vector (DESIGN.md §6.7): single-source shortest-path +-- distances by Bellman-Ford relaxation (min_plus SpMV along edges, +-- accumulating min) until no distance falls, or after max_iter iterations. +-- By default the bound is the live node count, and distances still falling +-- then mean a negative cycle reachable from the source, an error (22023). +-- Weights as spmv reads them (1 if unweighted); unreachable nodes are +-- Infinity. Writes vector `out` (f64) and returns the iterations run. +CREATE FUNCTION warren.sssp_vector(graph text, "out" text, source anyelement, + rels text[] DEFAULT NULL, weighted boolean DEFAULT NULL, + max_iter int DEFAULT NULL, version bigint DEFAULT NULL) +RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ +DECLARE + v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); + parts text[] := ARRAY['x', 'next', 'fell']; + limit_ int; + falling float8; + i int := 0; BEGIN IF starts_with("out", '_warren_') THEN RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' USING ERRCODE = 'reserved_name'; END IF; - RETURN warren.vector_degree(graph, "out", direction, rels, 'f64', true, version); + BEGIN + PERFORM warren.vector_from_query(graph, '_warren_sssp_vector_x', + format('SELECT %L::%s, 0', source, pg_typeof(source)), + fill := 'Infinity', persist := false, version := v); + IF warren.vector_reduce(graph, '_warren_sssp_vector_x', 'min', v) <> 0 THEN + RAISE EXCEPTION 'node % is not a node of graph "%"', source, graph USING ERRCODE = 'invalid_parameter_value'; + END IF; + limit_ := coalesce(max_iter, (warren.vector_stats(graph, '_warren_sssp_vector_x', v) ->> 'count')::int); + WHILE i < limit_ LOOP + i := i + 1; + PERFORM warren.vector_map(graph, '_warren_sssp_vector_next', '_warren_sssp_vector_x', false, v); + PERFORM warren.spmv(graph, '_warren_sssp_vector_x', '_warren_sssp_vector_next', 'min_plus', + transpose := true, rels := rels, weighted := weighted, accumulate := 'min', + persist := false, version := v); + PERFORM warren.vector_map(graph, '_warren_sssp_vector_fell', + 'CASE WHEN _warren_sssp_vector_next < _warren_sssp_vector_x THEN 1 ELSE 0 END', false, v); + falling := warren.vector_reduce(graph, '_warren_sssp_vector_fell', 'sum', v); + PERFORM warren.vector_map(graph, '_warren_sssp_vector_x', '_warren_sssp_vector_next', false, v); + EXIT WHEN falling = 0; + END LOOP; + IF max_iter IS NULL AND falling > 0 THEN + RAISE EXCEPTION 'a negative cycle is reachable from node % of graph "%"', source, graph + USING ERRCODE = 'invalid_parameter_value'; + END IF; + PERFORM warren.vector_map(graph, "out", '_warren_sssp_vector_x', true, v); + EXCEPTION WHEN OTHERS OR query_canceled THEN + PERFORM warren.drop_vector(graph, '_warren_sssp_vector_' || part, v, true) FROM unnest(parts) part; + RAISE; + END; + PERFORM warren.drop_vector(graph, '_warren_sssp_vector_' || part, v, true) FROM unnest(parts) part; + RETURN i; END $$; /* */ @@ -685,145 +645,207 @@ CREATE CAST (jsonb AS warren.step[]) WITH FUNCTION warren.steps(jsonb); /* */ /* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:41 +-- crates/pg_warren/src/compute_sql/library/mod.rs:8 -- requires: -- extension::warren --- warren.label_propagation_majority (DESIGN.md §6.7): majority-vote label --- propagation, synchronous and deterministic. Labels start as vector `seed` --- (default: each node's dense id); each iteration every node takes the most --- frequent label among its own and its neighbours' (direction ignored, each --- neighbour once, over `rels`; a self-loop counts the node's own label once --- more; weights are not read), the smallest on a tie, until no label --- changes, or after max_iter iterations with a WARNING (a synchronous --- update can oscillate). NetworkX's label propagation is asynchronous --- (random order and ties) or semi-synchronous (by colouring), so neither --- matches step for step. Tier 2: each iteration is one `gather` (mode, with --- include_self), which counts the labels in-engine, N rows through the --- executor. Writes vector `out` (f64) and returns the iterations run. -CREATE FUNCTION warren.label_propagation_majority(graph text, "out" text, seed text DEFAULT NULL, - max_iter int DEFAULT 100, rels text[] DEFAULT NULL, - version bigint DEFAULT NULL) -RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ +-- warren.degree_vector (DESIGN.md §6.7): each node's degree in `direction` +-- over `rels`, as vector `out` (f64); returns its vector id. +CREATE FUNCTION warren.degree_vector(graph text, "out" text, direction text DEFAULT 'out', + rels text[] DEFAULT NULL, version bigint DEFAULT NULL) +RETURNS bigint LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ +BEGIN + IF starts_with("out", '_warren_') THEN + RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' + USING ERRCODE = 'reserved_name'; + END IF; + RETURN warren.vector_degree(graph, "out", direction, rels, 'f64', true, version); +END $$; +/* */ + +/* */ +-- crates/pg_warren/src/community_sql/library/mod.rs:9 +-- requires: +-- extension::warren + +-- warren.leiden (DESIGN.md §6.7): Leiden community detection (Traag, +-- Waltman and van Eck 2019) as a library over the primitives. Each level +-- moves nodes between communities (warren.local_moving, from the partition +-- the level below found) on the level's graph, refines each community +-- into well-connected parts (warren.leiden_refine), lifts both partitions +-- to `graph` (warren.lift), and contracts `graph` by the refined one +-- (warren.contract) into the next level's graph; the next level starts +-- from the unrefined partition, projected onto it (warren.project, min). +-- As warren.louvain's, every level's graph is a quotient of `graph`, and +-- only one exists at a time. Every community found is connected. As +-- Traag et al.'s, it goes on while there is something to aggregate: it +-- stops when a level's local moving leaves every node alone, when the +-- refinement leaves every node alone (the next level's graph would be this +-- one), or after `max_levels`; in the last two cases it returns the +-- refined partition, so every exit returns a connected one. A level's +-- local moving stops when a sweep raises modularity by no more than +-- `tolerance`. That multilevel pass is one iteration; each later one +-- starts its first level's moving from the partition the one before found, +-- not from each node alone, as igraph's `n_iterations` and leidenalg's +-- `optimise_partition` loop do. The run stops after `iterations`, or +-- after an iteration whose partition is the one it started from: with the +-- same start and seed, the next would find it again. A negative +-- `iterations` runs until then, at most 100 iterations (a WARNING if it +-- is still changing). Arguments and output as warren.louvain's; +-- `randomness` is θ of the refinement. `levels_prefix` keeps every +-- iteration's levels in turn (`_1`, … numbered on across +-- iterations), so the last is `out`. The level's graph, a level graph of +-- warren.contract's (`_warren_level_`), is dropped at the end of +-- each iteration. +CREATE FUNCTION warren.leiden(graph text, "out" text, resolution float8 DEFAULT 1.0, + seed bigint DEFAULT 0, max_levels int DEFAULT 100, + tolerance float8 DEFAULT 1e-7, randomness float8 DEFAULT 0.01, + iterations int DEFAULT 2, weighted boolean DEFAULT NULL, levels_prefix text DEFAULT NULL, + version bigint DEFAULT NULL) +RETURNS float8 LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ DECLARE - info jsonb := warren.info(graph); - v bigint := coalesce(version, (info ->> 'current_version')::bigint); - parts text[] := ARRAY['x', 'next']; - -- Each node's own label and its neighbours', counted in-engine; the most - -- votes win, then the smallest label. - votes text := format( - 'SELECT node_id, value - FROM warren.gather(%1$L, %2$L, NULL::%3$s, NULL::float8, ''mode'', ''both'', %4$L::text[], - version := %5$s, include_self := true)', - graph, '_warren_label_propagation_majority_x', info ->> 'id_type', rels, v); + v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); + parts text[] := ARRAY['moved', 'refined', 'initial', 'keys', 'down', 'refined_down', 'prev', 'changed']; + g text := graph; + gv bigint := v; + next_g text; + next_v bigint; + level int; + iteration int := 0; + written int := 0; nodes bigint; - delta float8; - i int := 0; + found bigint; + refined bigint; + q float8; BEGIN - IF "out" IS NULL OR max_iter IS NULL THEN - RAISE EXCEPTION '% must not be NULL', CASE WHEN "out" IS NULL THEN 'out' ELSE 'max_iter' END + IF "out" IS NULL OR resolution IS NULL OR seed IS NULL OR max_levels IS NULL OR tolerance IS NULL + OR randomness IS NULL OR iterations IS NULL THEN + RAISE EXCEPTION '% must not be NULL', + CASE WHEN "out" IS NULL THEN 'out' WHEN resolution IS NULL THEN 'resolution' + WHEN seed IS NULL THEN 'seed' WHEN max_levels IS NULL THEN 'max_levels' + WHEN tolerance IS NULL THEN 'tolerance' WHEN randomness IS NULL THEN 'randomness' + ELSE 'iterations' END USING ERRCODE = 'null_value_not_allowed'; END IF; - IF starts_with("out", '_warren_') OR starts_with(seed, '_warren_') THEN + IF starts_with("out", '_warren_') OR starts_with(levels_prefix, '_warren_') THEN RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' USING ERRCODE = 'reserved_name'; END IF; - IF max_iter < 1 THEN - RAISE EXCEPTION 'max_iter must be at least 1, not %', max_iter USING ERRCODE = 'invalid_parameter_value'; + IF max_levels < 1 THEN + RAISE EXCEPTION 'max_levels must be at least 1, not %', max_levels USING ERRCODE = 'invalid_parameter_value'; + END IF; + IF iterations = 0 THEN + RAISE EXCEPTION 'iterations must be at least 1, or negative to run until the partition is stable, not 0' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + IF tolerance = 'NaN'::float8 OR tolerance < 0 THEN + RAISE EXCEPTION 'tolerance must be a number of at least 0, not %', tolerance + USING ERRCODE = 'invalid_parameter_value'; END IF; BEGIN - IF seed IS NULL THEN - PERFORM warren.vector_dense_id(graph, '_warren_label_propagation_majority_x', 'f64', false, v); - ELSE - -- vector_reduce names a missing seed vector (42704); vector_map would - -- call it a missing column. - PERFORM warren.vector_reduce(graph, seed, 'nnz', v); - PERFORM warren.vector_map(graph, '_warren_label_propagation_majority_x', format('%I::float8', seed), false, v); - END IF; - -- No live nodes, no labels: no iterations (NetworkX finds no communities). - nodes := (warren.vector_stats(graph, '_warren_label_propagation_majority_x', v) ->> 'count')::bigint; - WHILE i < max_iter AND nodes > 0 LOOP - i := i + 1; - PERFORM warren.vector_from_query(graph, '_warren_label_propagation_majority_next', votes, 'f64', - persist := false, version := v); - delta := warren.vector_norm_diff(graph, '_warren_label_propagation_majority_next', - '_warren_label_propagation_majority_x', 'l1', v, v); - PERFORM warren.vector_map(graph, '_warren_label_propagation_majority_x', - '_warren_label_propagation_majority_next', false, v); - EXIT WHEN delta = 0; + LOOP + iteration := iteration + 1; + -- A later iteration's first level moves from the last partition. + IF iteration > 1 THEN + PERFORM warren.vector_map(graph, '_warren_leiden_prev', '_warren_leiden_down', false, v); + END IF; + level := 0; + refined := NULL; + LOOP + level := level + 1; + nodes := (SELECT (e ->> 'node_count')::bigint FROM jsonb_array_elements(warren.info(g) -> 'versions') e + WHERE (e ->> 'version')::bigint = gv); + IF level > 1 THEN + PERFORM warren.vector_from_query(g, '_warren_leiden_keys', + format('SELECT node_id, node_id FROM warren.id_map(%L, NULL::bigint, %s)', g, gv), 'i64', + persist := false, version := gv); + END IF; + found := warren.local_moving(g, '_warren_leiden_moved', + initial := CASE WHEN level > 1 THEN '_warren_leiden_initial' + WHEN iteration > 1 THEN '_warren_leiden_prev' END, + keys := CASE WHEN level > 1 THEN '_warren_leiden_keys' END, + resolution := resolution, seed := seed, tolerance := tolerance, + weighted := CASE WHEN level = 1 THEN weighted ELSE true END, + persist := false, version := gv); + -- This level's partition, on `graph`. + IF level = 1 THEN + PERFORM warren.vector_map(graph, '_warren_leiden_down', '_warren_leiden_moved', false, v); + ELSE + PERFORM warren.lift(g, '_warren_leiden_moved', graph, '_warren_leiden_down', + persist := false, version := gv); + END IF; + -- Every node alone: the partition is this level's nodes, the refined + -- partition of the level below, which is connected. + IF found < nodes THEN + refined := warren.leiden_refine(g, '_warren_leiden_moved', '_warren_leiden_refined', + keys := CASE WHEN level > 1 THEN '_warren_leiden_keys' END, + resolution := resolution, seed := seed, randomness := randomness, + weighted := CASE WHEN level = 1 THEN weighted ELSE true END, + persist := false, version := gv); + IF level = 1 THEN + PERFORM warren.vector_map(graph, '_warren_leiden_refined_down', '_warren_leiden_refined', false, v); + ELSE + PERFORM warren.lift(g, '_warren_leiden_refined', graph, '_warren_leiden_refined_down', + persist := false, version := gv); + END IF; + -- Stopping here, return the refined partition, which is connected, + -- as Traag et al.'s is; the moved one need not be. + IF level >= max_levels OR refined = nodes THEN + PERFORM warren.vector_map(graph, '_warren_leiden_down', '_warren_leiden_refined_down', false, v); + END IF; + END IF; + IF levels_prefix IS NOT NULL THEN + written := written + 1; + PERFORM warren.vector_map(graph, format('%s_%s', levels_prefix, written), '_warren_leiden_down', true, v); + END IF; + EXIT WHEN found = nodes OR level >= max_levels OR refined = nodes; + -- A level graph of its own, named after its version, a name no + -- graph can have had. + next_v := warren.contract(graph, '_warren_leiden_refined_down', NULL, + weighted := coalesce(weighted, true), version := v); + next_g := '_warren_level_' || next_v; + -- The next level starts from the unrefined partition: every member + -- of a refined community is in the same community, so min is it. + PERFORM warren.project(graph, '_warren_leiden_down', next_g, '_warren_leiden_initial', 'min', + persist := false, version := next_v); + IF g <> graph THEN + PERFORM warren.drop_vector(g, '_warren_leiden_' || part, gv, true) FROM unnest(parts) part; + PERFORM warren.drop_graph(g); + END IF; + g := next_g; + gv := next_v; + END LOOP; + IF g <> graph THEN + PERFORM warren.drop_vector(g, '_warren_leiden_' || part, gv, true) FROM unnest(parts) part; + PERFORM warren.drop_graph(g); + g := graph; + gv := v; + END IF; + EXIT WHEN iteration = iterations; + -- Labels are the least dense id of each community's members, so the + -- partitions are the same exactly when the labels are. + IF iteration > 1 THEN + PERFORM warren.vector_map(graph, '_warren_leiden_changed', + 'CASE WHEN _warren_leiden_down = _warren_leiden_prev THEN 0 ELSE 1 END', false, v); + EXIT WHEN warren.vector_reduce(graph, '_warren_leiden_changed', 'sum', v) = 0; + END IF; + IF iterations < 0 AND iteration = 100 THEN + RAISE WARNING 'leiden did not stabilise in 100 iterations'; + EXIT; + END IF; END LOOP; - IF delta <> 0 THEN - RAISE WARNING 'label_propagation_majority did not converge in % iterations', i; - END IF; - PERFORM warren.vector_map(graph, "out", '_warren_label_propagation_majority_x', true, v); + PERFORM warren.vector_map(graph, "out", '_warren_leiden_down', true, v); + q := warren.modularity(graph, "out", resolution, weighted, v); EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_label_propagation_majority_' || part, v, true) - FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_leiden_' || part, v, true) FROM unnest(parts) part; RAISE; END; - PERFORM warren.drop_vector(graph, '_warren_label_propagation_majority_' || part, v, true) FROM unnest(parts) part; - RETURN i; -END $$; -/* */ - -/* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:23 --- requires: --- extension::warren - --- warren.sssp_vector (DESIGN.md §6.7): single-source shortest-path --- distances by Bellman-Ford relaxation (min_plus SpMV along edges, --- accumulating min) until no distance falls, or after max_iter iterations. --- By default the bound is the live node count, and distances still falling --- then mean a negative cycle reachable from the source, an error (22023). --- Weights as spmv reads them (1 if unweighted); unreachable nodes are --- Infinity. Writes vector `out` (f64) and returns the iterations run. -CREATE FUNCTION warren.sssp_vector(graph text, "out" text, source anyelement, - rels text[] DEFAULT NULL, weighted boolean DEFAULT NULL, - max_iter int DEFAULT NULL, version bigint DEFAULT NULL) -RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ -DECLARE - v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); - parts text[] := ARRAY['x', 'next', 'fell']; - limit_ int; - falling float8; - i int := 0; -BEGIN - IF starts_with("out", '_warren_') THEN - RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' - USING ERRCODE = 'reserved_name'; + PERFORM warren.drop_vector(graph, '_warren_leiden_' || part, v, true) FROM unnest(parts) part; + IF g <> graph THEN + PERFORM warren.drop_vector(g, '_warren_leiden_' || part, gv, true) FROM unnest(parts) part; + PERFORM warren.drop_graph(g); END IF; - BEGIN - PERFORM warren.vector_from_query(graph, '_warren_sssp_vector_x', - format('SELECT %L::%s, 0', source, pg_typeof(source)), - fill := 'Infinity', persist := false, version := v); - IF warren.vector_reduce(graph, '_warren_sssp_vector_x', 'min', v) <> 0 THEN - RAISE EXCEPTION 'node % is not a node of graph "%"', source, graph USING ERRCODE = 'invalid_parameter_value'; - END IF; - limit_ := coalesce(max_iter, (warren.vector_stats(graph, '_warren_sssp_vector_x', v) ->> 'count')::int); - WHILE i < limit_ LOOP - i := i + 1; - PERFORM warren.vector_map(graph, '_warren_sssp_vector_next', '_warren_sssp_vector_x', false, v); - PERFORM warren.spmv(graph, '_warren_sssp_vector_x', '_warren_sssp_vector_next', 'min_plus', - transpose := true, rels := rels, weighted := weighted, accumulate := 'min', - persist := false, version := v); - PERFORM warren.vector_map(graph, '_warren_sssp_vector_fell', - 'CASE WHEN _warren_sssp_vector_next < _warren_sssp_vector_x THEN 1 ELSE 0 END', false, v); - falling := warren.vector_reduce(graph, '_warren_sssp_vector_fell', 'sum', v); - PERFORM warren.vector_map(graph, '_warren_sssp_vector_x', '_warren_sssp_vector_next', false, v); - EXIT WHEN falling = 0; - END LOOP; - IF max_iter IS NULL AND falling > 0 THEN - RAISE EXCEPTION 'a negative cycle is reachable from node % of graph "%"', source, graph - USING ERRCODE = 'invalid_parameter_value'; - END IF; - PERFORM warren.vector_map(graph, "out", '_warren_sssp_vector_x', true, v); - EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_sssp_vector_' || part, v, true) FROM unnest(parts) part; - RAISE; - END; - PERFORM warren.drop_vector(graph, '_warren_sssp_vector_' || part, v, true) FROM unnest(parts) part; - RETURN i; + RETURN q; END $$; /* */ @@ -948,155 +970,118 @@ END $$; /* */ /* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:39 +-- crates/pg_warren/src/compute_sql/library/mod.rs:18 -- requires: -- extension::warren --- warren.hits (DESIGN.md §6.7): HITS hubs and authorities by power --- iteration, as NetworkX computes them (its default, method := --- 'power_iteration'). From hubs uniform at 1/N, each iteration takes the --- authorities a = Aᵀh (a node pointed at by good hubs) and the hubs h = A·a --- (a node pointing at good authorities), weighted by edge weight (spmv's --- rule: weights if the graph has them, unless weighted := false), and --- scales h to a largest value of 1; it stops when the L1 change of h falls --- below tol (not scaled by N, as NetworkX), or after max_iter iterations --- with a WARNING (NetworkX raises). With normalized, each result sums to --- 1; otherwise each has a largest value of 1. A zero sum or maximum (no --- edges) leaves the zeros as they are. Values too large for double --- precision are refused (22003) and write nothing. Writes vectors `hubs` --- and `authorities` (f64) and returns the iterations run. A constant --- vector is 0 at tombstoned rows, where vector_map evaluates too, so a --- division by one is guarded. -CREATE FUNCTION warren.hits(graph text, hubs text, authorities text, tol float8 DEFAULT 1e-8, - max_iter int DEFAULT 100, normalized boolean DEFAULT true, - rels text[] DEFAULT NULL, weighted boolean DEFAULT NULL, - version bigint DEFAULT NULL) +-- warren.label_propagation (DESIGN.md §6.7, A.1): min-label propagation. +-- Labels start as vector `seed` (default: each node's dense id); each +-- iteration every node takes the smallest label among itself and its +-- neighbours (both directions, over `rels`), until no label changes or +-- after max_iter iterations. With `mask`, only the nodes it selects (a +-- non-zero value, GraphBLAS semantics) send or take labels; the others +-- keep theirs. It is deterministic min-propagation, not majority-vote +-- label propagation. Writes vector `out` (f64) and returns the iterations +-- run. +CREATE FUNCTION warren.label_propagation(graph text, "out" text, seed text DEFAULT NULL, + max_iter int DEFAULT 100, mask text DEFAULT NULL, + rels text[] DEFAULT NULL, version bigint DEFAULT NULL) RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ DECLARE v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); - parts text[] := ARRAY['one', 'x', 'a', 'next', 'scale']; - nodes float8; - scale float8; - hub_scale float8; + parts text[] := ARRAY['x', 'msg', 'next']; + selected text; delta float8; i int := 0; BEGIN - IF hubs IS NULL OR authorities IS NULL OR normalized IS NULL OR tol IS NULL OR max_iter IS NULL THEN - RAISE EXCEPTION '% must not be NULL', - CASE WHEN hubs IS NULL THEN 'hubs' WHEN authorities IS NULL THEN 'authorities' - WHEN normalized IS NULL THEN 'normalized' WHEN tol IS NULL THEN 'tol' ELSE 'max_iter' END - USING ERRCODE = 'null_value_not_allowed'; - END IF; - IF starts_with(hubs, '_warren_') OR starts_with(authorities, '_warren_') THEN + IF starts_with("out", '_warren_') OR starts_with(seed, '_warren_') OR starts_with(mask, '_warren_') THEN RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' USING ERRCODE = 'reserved_name'; END IF; - IF hubs = authorities THEN - RAISE EXCEPTION 'hubs and authorities must be different vectors' USING ERRCODE = 'invalid_parameter_value'; - END IF; - IF tol < 0 THEN - RAISE EXCEPTION 'tol must be at least 0, not %', tol USING ERRCODE = 'invalid_parameter_value'; - END IF; - IF max_iter < 1 THEN - RAISE EXCEPTION 'max_iter must be at least 1, not %', max_iter USING ERRCODE = 'invalid_parameter_value'; - END IF; BEGIN - PERFORM warren.vector_const(graph, '_warren_hits_one', 1, persist := false, version := v); - nodes := warren.vector_reduce(graph, '_warren_hits_one', 'sum', v); - IF nodes = 0 THEN - RAISE EXCEPTION 'graph "%" has no live nodes', graph USING ERRCODE = 'invalid_parameter_value'; + IF seed IS NULL THEN + PERFORM warren.vector_dense_id(graph, '_warren_label_propagation_x', 'f64', false, v); + ELSE + PERFORM warren.vector_map(graph, '_warren_label_propagation_x', format('%I::float8', seed), false, v); + END IF; + IF mask IS NOT NULL THEN + selected := CASE warren.explain_map(graph, format('%I', mask), v) ->> 'dtype' + WHEN 'bool' THEN format('%I', mask) ELSE format('%I <> 0', mask) END; END IF; - PERFORM warren.vector_const(graph, '_warren_hits_x', 1.0 / nodes, persist := false, version := v); WHILE i < max_iter LOOP i := i + 1; - PERFORM warren.spmv(graph, '_warren_hits_x', '_warren_hits_a', transpose := true, rels := rels, - weighted := weighted, persist := false, version := v); - PERFORM warren.spmv(graph, '_warren_hits_a', '_warren_hits_next', rels := rels, weighted := weighted, - persist := false, version := v); - scale := warren.vector_reduce(graph, '_warren_hits_next', 'max', v); - IF NOT abs(scale) < 'Infinity' THEN - RAISE EXCEPTION 'hits diverged: its values are not finite after % iterations', i - USING ERRCODE = 'numeric_value_out_of_range', - HINT = 'The edge weights are too large: their products overflow double precision.'; - END IF; - PERFORM warren.vector_const(graph, '_warren_hits_scale', CASE WHEN scale = 0 THEN 1 ELSE scale END, - persist := false, version := v); - PERFORM warren.vector_map(graph, '_warren_hits_next', - 'CASE WHEN _warren_hits_scale <> 0 THEN _warren_hits_next / _warren_hits_scale ELSE 0 END', false, v); - delta := warren.vector_norm_diff(graph, '_warren_hits_next', '_warren_hits_x', 'l1', v, v); - PERFORM warren.vector_map(graph, '_warren_hits_x', '_warren_hits_next', false, v); - EXIT WHEN delta < tol; + -- Unselected nodes send NULL (NaN), which min never picks over a label. + PERFORM warren.vector_map(graph, '_warren_label_propagation_msg', + CASE WHEN selected IS NULL THEN '_warren_label_propagation_x' + ELSE format('CASE WHEN %s THEN _warren_label_propagation_x END', selected) END, false, v); + PERFORM warren.vector_map(graph, '_warren_label_propagation_next', '_warren_label_propagation_x', false, v); + PERFORM warren.spmv(graph, '_warren_label_propagation_msg', '_warren_label_propagation_next', 'min_second', + rels := rels, mask := mask, accumulate := 'min', persist := false, version := v); + PERFORM warren.spmv(graph, '_warren_label_propagation_msg', '_warren_label_propagation_next', 'min_second', + transpose := true, rels := rels, mask := mask, accumulate := 'min', persist := false, + version := v); + delta := warren.vector_norm_diff(graph, '_warren_label_propagation_next', '_warren_label_propagation_x', + 'l1', v, v); + PERFORM warren.vector_map(graph, '_warren_label_propagation_x', '_warren_label_propagation_next', false, v); + EXIT WHEN delta = 0; END LOOP; - IF NOT delta < tol THEN - RAISE WARNING 'hits did not converge in % iterations', i; - END IF; - IF normalized THEN - hub_scale := warren.vector_reduce(graph, '_warren_hits_x', 'sum', v); - scale := warren.vector_reduce(graph, '_warren_hits_a', 'sum', v); - ELSE - hub_scale := 1; - scale := warren.vector_reduce(graph, '_warren_hits_a', 'max', v); - END IF; - IF NOT abs(hub_scale) < 'Infinity' OR NOT abs(scale) < 'Infinity' THEN - RAISE EXCEPTION 'hits diverged: its values are not finite after % iterations', i - USING ERRCODE = 'numeric_value_out_of_range', - HINT = 'The edge weights are too large: their products overflow double precision.'; - END IF; - PERFORM warren.vector_const(graph, '_warren_hits_scale', CASE WHEN hub_scale = 0 THEN 1 ELSE hub_scale END, - persist := false, version := v); - PERFORM warren.vector_map(graph, hubs, - 'CASE WHEN _warren_hits_scale <> 0 THEN _warren_hits_x / _warren_hits_scale ELSE 0 END', true, v); - PERFORM warren.vector_const(graph, '_warren_hits_scale', CASE WHEN scale = 0 THEN 1 ELSE scale END, - persist := false, version := v); - PERFORM warren.vector_map(graph, authorities, - 'CASE WHEN _warren_hits_scale <> 0 THEN _warren_hits_a / _warren_hits_scale ELSE 0 END', true, v); + PERFORM warren.vector_map(graph, "out", '_warren_label_propagation_x', true, v); EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_hits_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_label_propagation_' || part, v, true) FROM unnest(parts) part; RAISE; END; - PERFORM warren.drop_vector(graph, '_warren_hits_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_label_propagation_' || part, v, true) FROM unnest(parts) part; RETURN i; END $$; /* */ /* */ --- crates/pg_warren/src/compute_sql/library/mod.rs:29 +-- crates/pg_warren/src/compute_sql/library/mod.rs:39 -- requires: -- extension::warren --- warren.eigenvector_centrality (DESIGN.md §6.7): eigenvector centrality --- by power iteration, as NetworkX computes it. Centrality flows along edges --- (the left eigenvector of A: a node is central when central nodes point at --- it), weighted by edge weight (spmv's rule: weights if the graph has them, --- unless weighted := false). From the uniform 1/N, each iteration takes --- x + Aᵀx (the shift by I keeps a periodic graph from oscillating) and --- scales it to unit L2 norm; it stops when the L1 change falls below --- (live nodes) × tol, or after max_iter iterations with a WARNING (NetworkX --- raises). Values too large for double precision (an L2 norm past about --- 1e154) are refused (22003) and write nothing. Writes vector `out` (f64, --- unit L2 norm) and returns the iterations run. Constants enter --- expressions as constant vectors; a constant vector is 0 at tombstoned --- rows, where vector_map evaluates too, so a division by one is guarded. -CREATE FUNCTION warren.eigenvector_centrality(graph text, "out" text, tol float8 DEFAULT 1e-6, - max_iter int DEFAULT 100, rels text[] DEFAULT NULL, - weighted boolean DEFAULT NULL, version bigint DEFAULT NULL) +-- warren.hits (DESIGN.md §6.7): HITS hubs and authorities by power +-- iteration, as NetworkX computes them (its default, method := +-- 'power_iteration'). From hubs uniform at 1/N, each iteration takes the +-- authorities a = Aᵀh (a node pointed at by good hubs) and the hubs h = A·a +-- (a node pointing at good authorities), weighted by edge weight (spmv's +-- rule: weights if the graph has them, unless weighted := false), and +-- scales h to a largest value of 1; it stops when the L1 change of h falls +-- below tol (not scaled by N, as NetworkX), or after max_iter iterations +-- with a WARNING (NetworkX raises). With normalized, each result sums to +-- 1; otherwise each has a largest value of 1. A zero sum or maximum (no +-- edges) leaves the zeros as they are. Values too large for double +-- precision are refused (22003) and write nothing. Writes vectors `hubs` +-- and `authorities` (f64) and returns the iterations run. A constant +-- vector is 0 at tombstoned rows, where vector_map evaluates too, so a +-- division by one is guarded. +CREATE FUNCTION warren.hits(graph text, hubs text, authorities text, tol float8 DEFAULT 1e-8, + max_iter int DEFAULT 100, normalized boolean DEFAULT true, + rels text[] DEFAULT NULL, weighted boolean DEFAULT NULL, + version bigint DEFAULT NULL) RETURNS int LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$ DECLARE v bigint := coalesce(version, (warren.info(graph) ->> 'current_version')::bigint); - parts text[] := ARRAY['one', 'x', 'y', 'next', 'scale']; + parts text[] := ARRAY['one', 'x', 'a', 'next', 'scale']; nodes float8; - norm float8; + scale float8; + hub_scale float8; delta float8; i int := 0; -BEGIN - IF "out" IS NULL OR tol IS NULL OR max_iter IS NULL THEN - RAISE EXCEPTION '% must not be NULL', CASE WHEN "out" IS NULL THEN 'out' WHEN tol IS NULL THEN 'tol' ELSE 'max_iter' END +BEGIN + IF hubs IS NULL OR authorities IS NULL OR normalized IS NULL OR tol IS NULL OR max_iter IS NULL THEN + RAISE EXCEPTION '% must not be NULL', + CASE WHEN hubs IS NULL THEN 'hubs' WHEN authorities IS NULL THEN 'authorities' + WHEN normalized IS NULL THEN 'normalized' WHEN tol IS NULL THEN 'tol' ELSE 'max_iter' END USING ERRCODE = 'null_value_not_allowed'; END IF; - IF starts_with("out", '_warren_') THEN + IF starts_with(hubs, '_warren_') OR starts_with(authorities, '_warren_') THEN RAISE EXCEPTION 'vector names beginning with _warren_ are reserved for the library''s intermediates' USING ERRCODE = 'reserved_name'; END IF; + IF hubs = authorities THEN + RAISE EXCEPTION 'hubs and authorities must be different vectors' USING ERRCODE = 'invalid_parameter_value'; + END IF; IF tol < 0 THEN RAISE EXCEPTION 'tol must be at least 0, not %', tol USING ERRCODE = 'invalid_parameter_value'; END IF; @@ -1104,57 +1089,112 @@ BEGIN RAISE EXCEPTION 'max_iter must be at least 1, not %', max_iter USING ERRCODE = 'invalid_parameter_value'; END IF; BEGIN - PERFORM warren.vector_const(graph, '_warren_eigenvector_centrality_one', 1, persist := false, version := v); - nodes := warren.vector_reduce(graph, '_warren_eigenvector_centrality_one', 'sum', v); + PERFORM warren.vector_const(graph, '_warren_hits_one', 1, persist := false, version := v); + nodes := warren.vector_reduce(graph, '_warren_hits_one', 'sum', v); IF nodes = 0 THEN RAISE EXCEPTION 'graph "%" has no live nodes', graph USING ERRCODE = 'invalid_parameter_value'; END IF; - PERFORM warren.vector_const(graph, '_warren_eigenvector_centrality_x', 1.0 / nodes, persist := false, - version := v); + PERFORM warren.vector_const(graph, '_warren_hits_x', 1.0 / nodes, persist := false, version := v); WHILE i < max_iter LOOP i := i + 1; - PERFORM warren.spmv(graph, '_warren_eigenvector_centrality_x', '_warren_eigenvector_centrality_y', - transpose := true, rels := rels, weighted := weighted, persist := false, version := v); - PERFORM warren.vector_map(graph, '_warren_eigenvector_centrality_next', - '_warren_eigenvector_centrality_x + _warren_eigenvector_centrality_y', false, v); - norm := warren.vector_reduce(graph, '_warren_eigenvector_centrality_next', 'l2', v); - IF NOT norm < 'Infinity' THEN - RAISE EXCEPTION 'eigenvector_centrality diverged: its values are not finite after % iterations', i + PERFORM warren.spmv(graph, '_warren_hits_x', '_warren_hits_a', transpose := true, rels := rels, + weighted := weighted, persist := false, version := v); + PERFORM warren.spmv(graph, '_warren_hits_a', '_warren_hits_next', rels := rels, weighted := weighted, + persist := false, version := v); + scale := warren.vector_reduce(graph, '_warren_hits_next', 'max', v); + IF NOT abs(scale) < 'Infinity' THEN + RAISE EXCEPTION 'hits diverged: its values are not finite after % iterations', i USING ERRCODE = 'numeric_value_out_of_range', - HINT = 'The edge weights are too large: their squares overflow double precision.'; + HINT = 'The edge weights are too large: their products overflow double precision.'; END IF; - PERFORM warren.vector_const(graph, '_warren_eigenvector_centrality_scale', - CASE WHEN norm = 0 THEN 1 ELSE norm END, persist := false, version := v); - PERFORM warren.vector_map(graph, '_warren_eigenvector_centrality_next', - 'CASE WHEN _warren_eigenvector_centrality_scale <> 0 - THEN _warren_eigenvector_centrality_next / _warren_eigenvector_centrality_scale ELSE 0 END', false, v); - delta := warren.vector_norm_diff(graph, '_warren_eigenvector_centrality_next', - '_warren_eigenvector_centrality_x', 'l1', v, v); - PERFORM warren.vector_map(graph, '_warren_eigenvector_centrality_x', '_warren_eigenvector_centrality_next', - false, v); - EXIT WHEN delta < nodes * tol; + PERFORM warren.vector_const(graph, '_warren_hits_scale', CASE WHEN scale = 0 THEN 1 ELSE scale END, + persist := false, version := v); + PERFORM warren.vector_map(graph, '_warren_hits_next', + 'CASE WHEN _warren_hits_scale <> 0 THEN _warren_hits_next / _warren_hits_scale ELSE 0 END', false, v); + delta := warren.vector_norm_diff(graph, '_warren_hits_next', '_warren_hits_x', 'l1', v, v); + PERFORM warren.vector_map(graph, '_warren_hits_x', '_warren_hits_next', false, v); + EXIT WHEN delta < tol; END LOOP; - IF NOT delta < nodes * tol THEN - RAISE WARNING 'eigenvector_centrality did not converge in % iterations', i; + IF NOT delta < tol THEN + RAISE WARNING 'hits did not converge in % iterations', i; END IF; - PERFORM warren.vector_map(graph, "out", '_warren_eigenvector_centrality_x', true, v); + IF normalized THEN + hub_scale := warren.vector_reduce(graph, '_warren_hits_x', 'sum', v); + scale := warren.vector_reduce(graph, '_warren_hits_a', 'sum', v); + ELSE + hub_scale := 1; + scale := warren.vector_reduce(graph, '_warren_hits_a', 'max', v); + END IF; + IF NOT abs(hub_scale) < 'Infinity' OR NOT abs(scale) < 'Infinity' THEN + RAISE EXCEPTION 'hits diverged: its values are not finite after % iterations', i + USING ERRCODE = 'numeric_value_out_of_range', + HINT = 'The edge weights are too large: their products overflow double precision.'; + END IF; + PERFORM warren.vector_const(graph, '_warren_hits_scale', CASE WHEN hub_scale = 0 THEN 1 ELSE hub_scale END, + persist := false, version := v); + PERFORM warren.vector_map(graph, hubs, + 'CASE WHEN _warren_hits_scale <> 0 THEN _warren_hits_x / _warren_hits_scale ELSE 0 END', true, v); + PERFORM warren.vector_const(graph, '_warren_hits_scale', CASE WHEN scale = 0 THEN 1 ELSE scale END, + persist := false, version := v); + PERFORM warren.vector_map(graph, authorities, + 'CASE WHEN _warren_hits_scale <> 0 THEN _warren_hits_a / _warren_hits_scale ELSE 0 END', true, v); EXCEPTION WHEN OTHERS OR query_canceled THEN - PERFORM warren.drop_vector(graph, '_warren_eigenvector_centrality_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_hits_' || part, v, true) FROM unnest(parts) part; RAISE; END; - PERFORM warren.drop_vector(graph, '_warren_eigenvector_centrality_' || part, v, true) FROM unnest(parts) part; + PERFORM warren.drop_vector(graph, '_warren_hits_' || part, v, true) FROM unnest(parts) part; RETURN i; END $$; /* */ /* */ --- crates/pg_warren/src/compute_sql/structure.rs:178 --- pg_warren::compute_sql::structure::minimum_spanning_forest +-- crates/pg_warren/src/cache/shared/introspection.rs:136 +-- pg_warren::cache::shared::introspection::shared_cache +CREATE FUNCTION warren."shared_cache"() RETURNS TABLE ( + "kind" TEXT, /* String */ + "graph" TEXT, /* Option < String > */ + "version" bigint, /* Option < i64 > */ + "vector" TEXT, /* Option < String > */ + "vector_id" bigint, /* Option < i64 > */ + "state" TEXT, /* Option < String > */ + "bytes" bigint, /* i64 */ + "pins" INT, /* i32 */ + "mappers" INT, /* i32 */ + "last_use" bigint, /* i64 */ + "cap" bigint /* Option < i64 > */ +) +STRICT VOLATILE PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'shared_cache_wrapper'; +/* */ -CREATE FUNCTION warren.minimum_spanning_forest(graph text, id_type anyelement, rels text[] DEFAULT NULL, - weighted bool DEFAULT NULL, version bigint DEFAULT NULL) -RETURNS TABLE (source anyelement, target anyelement, weight float8) -STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'minimum_spanning_forest_wrapper'; +/* */ +-- crates/pg_warren/src/catalog/dropped_roles/orphans.rs:34 +-- pg_warren::catalog::dropped_roles::orphans::orphans +CREATE FUNCTION warren."orphans"() RETURNS TABLE ( + "kind" TEXT, /* String */ + "graph" TEXT, /* String */ + "role" oid /* pg_sys :: Oid */ +) +STRICT VOLATILE PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'orphans_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/compute_sql/structure.rs:139 +-- pg_warren::compute_sql::structure::dag_longest_path +CREATE FUNCTION warren."dag_longest_path"( + "graph" TEXT, /* Option < & str > */ + "out" TEXT, /* Option < & str > */ + "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ + "weighted" bool DEFAULT NULL, /* Option < bool > */ + "persist" bool DEFAULT true, /* Option < bool > */ + "version" bigint DEFAULT NULL /* Option < i64 > */ +) RETURNS double precision /* Option < f64 > */ +VOLATILE PARALLEL UNSAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'dag_longest_path_wrapper'; /* */ /* */ @@ -1166,181 +1206,312 @@ STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'grant_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/paths.rs:69 --- pg_warren::traversal_sql::paths::shortest_path +-- crates/pg_warren/src/traversal_sql/components.rs:21 +-- pg_warren::traversal_sql::components::components -CREATE FUNCTION warren.shortest_path(graph text, start anyelement, target anyelement, - max_depth int DEFAULT NULL, rels text[] DEFAULT NULL, - direction text DEFAULT 'out', version bigint DEFAULT NULL) -RETURNS anyarray -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'shortest_path_wrapper'; +CREATE FUNCTION warren.components(graph text, id_type anyelement, version bigint DEFAULT NULL) +RETURNS TABLE (node_id anyelement, component_id bigint) +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'components_wrapper'; /* */ /* */ --- crates/pg_warren/src/community_sql/contract.rs:39 --- pg_warren::community_sql::contract::contract -CREATE FUNCTION warren."contract"( +-- crates/pg_warren/src/catalog/create_graph.rs:42 +-- pg_warren::catalog::create_graph::create_graph +CREATE FUNCTION warren."create_graph"( + "name" TEXT, /* & str */ + "node_query" TEXT, /* & str */ + "edge_query" TEXT, /* & str */ + "directed" bool DEFAULT true, /* bool */ + "weighted" bool DEFAULT false, /* bool */ + "has_relations" bool DEFAULT false, /* bool */ + "build_reverse" bool DEFAULT true, /* bool */ + "retain_versions" INT DEFAULT 2, /* i32 */ + "store_merged" bool DEFAULT true, /* bool */ + "node_order" TEXT DEFAULT 'id' /* & str */ +) RETURNS void +STRICT +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'create_graph_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/vector/quota/sql.rs:85 +-- pg_warren::vector::quota::sql::vector_usage + +CREATE FUNCTION warren."vector_usage"(name text) +RETURNS TABLE (role regrole, vectors bigint, bytes bigint, quota bigint) +STRICT STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'vector_usage_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/traversal_sql/paths.rs:114 +-- pg_warren::traversal_sql::paths::dijkstra + +CREATE FUNCTION warren.dijkstra(graph text, start anyelement, target anyelement, + rels text[] DEFAULT NULL, direction text DEFAULT 'out', + version bigint DEFAULT NULL) +RETURNS TABLE (path anyarray, cost float8) +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'dijkstra_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/compute_sql/constructors.rs:132 +-- pg_warren::compute_sql::constructors::vector_dense_id +CREATE FUNCTION warren."vector_dense_id"( "graph" TEXT, /* Option < & str > */ - "label_vector" TEXT, /* Option < & str > */ - "out_graph" TEXT, /* Option < & str > */ - "weighted" bool DEFAULT true, /* Option < bool > */ - "drop_self_loops" bool DEFAULT false, /* Option < bool > */ - "promote" bool DEFAULT true, /* Option < bool > */ + "name" TEXT, /* Option < & str > */ + "dtype" TEXT DEFAULT 'i64', /* Option < & str > */ + "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ ) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'contract_wrapper'; +AS 'MODULE_PATHNAME', 'vector_dense_id_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/read.rs:62 --- pg_warren::compute_sql::read::vector_values +-- crates/pg_warren/src/traversal_sql/paths.rs:25 +-- pg_warren::traversal_sql::paths::reachable -CREATE FUNCTION warren.vector_values(graph text, name text, id_type anyelement, - version bigint DEFAULT NULL) +CREATE FUNCTION warren.reachable(graph text, start anyelement, target anyelement, + max_depth int DEFAULT NULL, rels text[] DEFAULT NULL, + direction text DEFAULT 'out', version bigint DEFAULT NULL) +RETURNS boolean +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'reachable_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/compute_sql/summary.rs:87 +-- pg_warren::compute_sql::summary::vector_stats +CREATE FUNCTION warren."vector_stats"( + "graph" TEXT, /* Option < & str > */ + "name" TEXT, /* Option < & str > */ + "version" bigint DEFAULT NULL /* Option < i64 > */ +) RETURNS jsonb /* JsonB */ +STABLE PARALLEL RESTRICTED +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'vector_stats_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/extension.rs:37 +-- pg_warren::extension::version +CREATE FUNCTION warren."version"() RETURNS TEXT /* & '_ str */ +STRICT STABLE PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'version_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/compute_sql/summary.rs:20 +-- pg_warren::compute_sql::summary::vector_topk + +CREATE FUNCTION warren.vector_topk(graph text, name text, k int, id_type anyelement, + descending bool DEFAULT true, version bigint DEFAULT NULL) RETURNS TABLE (node_id anyelement, value float8) -STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'vector_values_wrapper'; +STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'vector_topk_wrapper'; /* */ /* */ --- crates/pg_warren/src/cache/shared/introspection.rs:136 --- pg_warren::cache::shared::introspection::shared_cache -CREATE FUNCTION warren."shared_cache"() RETURNS TABLE ( - "kind" TEXT, /* String */ - "graph" TEXT, /* Option < String > */ - "version" bigint, /* Option < i64 > */ - "vector" TEXT, /* Option < String > */ - "vector_id" bigint, /* Option < i64 > */ - "state" TEXT, /* Option < String > */ - "bytes" bigint, /* i64 */ - "pins" INT, /* i32 */ - "mappers" INT, /* i32 */ - "last_use" bigint, /* i64 */ - "cap" bigint /* Option < i64 > */ -) -STRICT VOLATILE PARALLEL SAFE +-- crates/pg_warren/src/compute_sql/map/mod.rs:156 +-- pg_warren::compute_sql::map::explain_map +CREATE FUNCTION warren."explain_map"( + "graph" TEXT, /* Option < & str > */ + "expr" TEXT, /* Option < & str > */ + "version" bigint DEFAULT NULL /* Option < i64 > */ +) RETURNS jsonb /* JsonB */ +STABLE PARALLEL RESTRICTED LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'shared_cache_wrapper'; +AS 'MODULE_PATHNAME', 'explain_map_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/neighbourhood.rs:23 --- pg_warren::traversal_sql::neighbourhood::neighbours +-- crates/pg_warren/src/build/refresh.rs:20 +-- pg_warren::build::refresh::refresh +CREATE FUNCTION warren."refresh"( + "name" TEXT, /* & str */ + "promote" bool DEFAULT true, /* bool */ + "label" TEXT DEFAULT NULL /* Option < & str > */ +) RETURNS bigint /* i64 */ -CREATE FUNCTION warren.neighbours(graph text, node anyelement, direction text DEFAULT 'out', - rels text[] DEFAULT NULL, version bigint DEFAULT NULL) -RETURNS SETOF anyelement -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'neighbours_wrapper'; +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'refresh_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/constructors.rs:132 --- pg_warren::compute_sql::constructors::vector_dense_id -CREATE FUNCTION warren."vector_dense_id"( +-- crates/pg_warren/src/compute_sql/from_query.rs:57 +-- pg_warren::compute_sql::from_query::vector_from_query +CREATE FUNCTION warren."vector_from_query"( "graph" TEXT, /* Option < & str > */ "name" TEXT, /* Option < & str > */ - "dtype" TEXT DEFAULT 'i64', /* Option < & str > */ + "query" TEXT, /* Option < & str > */ + "dtype" TEXT DEFAULT 'f64', /* Option < & str > */ + "fill" double precision DEFAULT 0, /* Option < f64 > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ ) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'vector_dense_id_wrapper'; +AS 'MODULE_PATHNAME', 'vector_from_query_wrapper'; /* */ /* */ --- crates/pg_warren/src/community_sql/local_moving.rs:96 --- pg_warren::community_sql::local_moving::leiden_refine -CREATE FUNCTION warren."leiden_refine"( +-- crates/pg_warren/src/compute_sql/reduce.rs:19 +-- pg_warren::compute_sql::reduce::vector_reduce +CREATE FUNCTION warren."vector_reduce"( "graph" TEXT, /* Option < & str > */ - "communities" TEXT, /* Option < & str > */ - "out" TEXT, /* Option < & str > */ - "keys" TEXT DEFAULT NULL, /* Option < & str > */ - "resolution" double precision DEFAULT 1.0, /* Option < f64 > */ - "seed" bigint DEFAULT 0, /* Option < i64 > */ - "randomness" double precision DEFAULT 0.01, /* Option < f64 > */ - "weighted" bool DEFAULT NULL, /* Option < bool > */ - "persist" bool DEFAULT true, /* Option < bool > */ + "x" TEXT, /* Option < & str > */ + "op" TEXT, /* Option < & str > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS bigint /* Option < i64 > */ -VOLATILE PARALLEL UNSAFE +) RETURNS double precision /* Option < f64 > */ +STABLE PARALLEL RESTRICTED LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'leiden_refine_wrapper'; +AS 'MODULE_PATHNAME', 'vector_reduce_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/create_graph.rs:41 --- pg_warren::catalog::create_graph::create_graph -CREATE FUNCTION warren."create_graph"( - "name" TEXT, /* & str */ - "node_query" TEXT, /* & str */ - "edge_query" TEXT, /* & str */ - "directed" bool DEFAULT true, /* bool */ - "weighted" bool DEFAULT false, /* bool */ - "has_relations" bool DEFAULT false, /* bool */ - "build_reverse" bool DEFAULT true, /* bool */ - "retain_versions" INT DEFAULT 2, /* i32 */ - "store_merged" bool DEFAULT true, /* bool */ - "node_order" TEXT DEFAULT 'id' /* & str */ -) RETURNS void -STRICT -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'create_graph_wrapper'; +-- crates/pg_warren/src/compute_sql/read.rs:132 +-- pg_warren::compute_sql::read::id_map + +CREATE FUNCTION warren.id_map(graph text, id_type anyelement, version bigint DEFAULT NULL) +RETURNS TABLE (node_id anyelement, dense_id bigint, live boolean) +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'id_map_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/constructors.rs:38 --- pg_warren::compute_sql::constructors::vector_const -CREATE FUNCTION warren."vector_const"( +-- crates/pg_warren/src/traversal_sql/neighbourhood.rs:52 +-- pg_warren::traversal_sql::neighbourhood::degree + +CREATE FUNCTION warren.degree(graph text, node anyelement, direction text DEFAULT 'out', + rels text[] DEFAULT NULL, version bigint DEFAULT NULL) +RETURNS bigint +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'degree_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/compute_sql/constructors.rs:163 +-- pg_warren::compute_sql::constructors::triangles +CREATE FUNCTION warren."triangles"( "graph" TEXT, /* Option < & str > */ - "name" TEXT, /* Option < & str > */ - "value" double precision, /* Option < f64 > */ - "dtype" TEXT DEFAULT 'f64', /* Option < & str > */ + "out" TEXT, /* Option < & str > */ + "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ ) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'vector_const_wrapper'; +AS 'MODULE_PATHNAME', 'triangles_wrapper'; /* */ /* */ --- crates/pg_warren/src/community_sql/local_moving.rs:29 --- pg_warren::community_sql::local_moving::local_moving -CREATE FUNCTION warren."local_moving"( +-- crates/pg_warren/src/spectral_sql/distance/mod.rs:96 +-- pg_warren::spectral_sql::distance::distance +CREATE FUNCTION warren."distance"( "graph" TEXT, /* Option < & str > */ - "out" TEXT, /* Option < & str > */ - "initial" TEXT DEFAULT NULL, /* Option < & str > */ - "keys" TEXT DEFAULT NULL, /* Option < & str > */ - "resolution" double precision DEFAULT 1.0, /* Option < f64 > */ - "seed" bigint DEFAULT 0, /* Option < i64 > */ - "max_sweeps" INT DEFAULT 100, /* Option < i32 > */ - "tolerance" double precision DEFAULT 1e-7, /* Option < f64 > */ + "v1" bigint, /* Option < i64 > */ + "v2" bigint, /* Option < i64 > */ + "method" TEXT, /* Option < & str > */ + "vectors_prefix" TEXT DEFAULT NULL, /* Option < & str > */ + "k" INT DEFAULT NULL, /* Option < i32 > */ + "x" TEXT DEFAULT NULL, /* Option < & str > */ + "y" TEXT DEFAULT NULL, /* Option < & str > */ + "compared_rows" TEXT DEFAULT 'shared', /* Option < & str > */ + "matrix" TEXT DEFAULT NULL, /* Option < & str > */ + "which" TEXT DEFAULT 'largest', /* Option < & str > */ + "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ "weighted" bool DEFAULT NULL, /* Option < bool > */ + "symmetrize" bool DEFAULT true, /* Option < bool > */ + "tol" double precision DEFAULT 1e-6, /* Option < f64 > */ + "max_iter" INT DEFAULT NULL, /* Option < i32 > */ + "dtype" TEXT DEFAULT 'f32', /* Option < & str > */ + "graph2" TEXT DEFAULT NULL /* Option < & str > */ +) RETURNS jsonb /* JsonB */ +STABLE PARALLEL RESTRICTED +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'distance_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/community_sql/projection.rs:60 +-- pg_warren::community_sql::projection::lift +CREATE FUNCTION warren."lift"( + "out_graph" TEXT, /* Option < & str > */ + "y" TEXT, /* Option < & str > */ + "graph" TEXT, /* Option < & str > */ + "x" TEXT, /* Option < & str > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS bigint /* Option < i64 > */ +) RETURNS void VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'local_moving_wrapper'; +AS 'MODULE_PATHNAME', 'lift_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/constructors.rs:93 --- pg_warren::compute_sql::constructors::vector_degree -CREATE FUNCTION warren."vector_degree"( +-- crates/pg_warren/src/compute_sql/reduce.rs:47 +-- pg_warren::compute_sql::reduce::vector_norm_diff +CREATE FUNCTION warren."vector_norm_diff"( "graph" TEXT, /* Option < & str > */ - "name" TEXT, /* Option < & str > */ - "direction" TEXT DEFAULT 'out', /* Option < & str > */ - "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ - "dtype" TEXT DEFAULT 'f64', /* Option < & str > */ + "a" TEXT, /* Option < & str > */ + "b" TEXT, /* Option < & str > */ + "norm" TEXT DEFAULT 'l2', /* Option < & str > */ + "a_version" bigint DEFAULT NULL, /* Option < i64 > */ + "b_version" bigint DEFAULT NULL /* Option < i64 > */ +) RETURNS double precision /* f64 */ +STABLE PARALLEL RESTRICTED +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'vector_norm_diff_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/compute_sql/gather/mod.rs:354 +-- pg_warren::compute_sql::gather::gather + +CREATE FUNCTION warren.gather(graph text, messages text, id_type anyelement, + value_type anycompatible, combine text DEFAULT 'collect', + direction text DEFAULT 'in', rels text[] DEFAULT NULL, + mask text DEFAULT NULL, mask_complement boolean DEFAULT false, + version bigint DEFAULT NULL, include_self boolean DEFAULT false) +RETURNS TABLE (node_id anyelement, value anycompatible) +VOLATILE PARALLEL UNSAFE LANGUAGE c AS 'MODULE_PATHNAME', 'gather_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/community_sql/projection.rs:26 +-- pg_warren::community_sql::projection::project +CREATE FUNCTION warren."project"( + "graph" TEXT, /* Option < & str > */ + "x" TEXT, /* Option < & str > */ + "out_graph" TEXT, /* Option < & str > */ + "y" TEXT, /* Option < & str > */ + "agg" TEXT DEFAULT 'sum', /* Option < & str > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS bigint /* Option < i64 > */ +) RETURNS void VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'vector_degree_wrapper'; +AS 'MODULE_PATHNAME', 'project_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/compute_sql/structure.rs:178 +-- pg_warren::compute_sql::structure::minimum_spanning_forest + +CREATE FUNCTION warren.minimum_spanning_forest(graph text, id_type anyelement, rels text[] DEFAULT NULL, + weighted bool DEFAULT NULL, version bigint DEFAULT NULL) +RETURNS TABLE (source anyelement, target anyelement, weight float8) +STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'minimum_spanning_forest_wrapper'; +/* */ + +/* */ +-- crates/pg_warren/src/traversal_sql/bfs.rs:87 +-- pg_warren::traversal_sql::bfs::reachable_set + +CREATE FUNCTION warren.reachable_set(graph text, start anyarray, max_depth int DEFAULT NULL, + rels text[] DEFAULT NULL, direction text DEFAULT 'out', + version bigint DEFAULT NULL) +RETURNS SETOF anyelement +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'reachable_set_wrapper'; /* */ /* */ @@ -1367,289 +1538,165 @@ STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'similar_pairs_wrapp /* */ /* */ --- crates/pg_warren/src/compute_sql/similarity.rs:48 --- pg_warren::compute_sql::similarity::similarity - -CREATE FUNCTION warren.similarity(graph text, pairs text, id_type anyelement, - measure text DEFAULT 'jaccard', rels text[] DEFAULT NULL, - version bigint DEFAULT NULL) -RETURNS TABLE (source anyelement, target anyelement, score float8) -STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'similarity_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/traversal_sql/stats.rs:30 --- pg_warren::traversal_sql::stats::last_stats -CREATE FUNCTION warren."last_stats"() RETURNS jsonb /* Option < JsonB > */ -STRICT VOLATILE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'last_stats_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/community_sql/modularity.rs:26 --- pg_warren::community_sql::modularity::modularity -CREATE FUNCTION warren."modularity"( - "graph" TEXT, /* Option < & str > */ - "labels" TEXT, /* Option < & str > */ - "resolution" double precision DEFAULT 1.0, /* Option < f64 > */ - "weighted" bool DEFAULT NULL, /* Option < bool > */ - "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS double precision /* Option < f64 > */ -STABLE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'modularity_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/extension.rs:37 --- pg_warren::extension::version -CREATE FUNCTION warren."version"() RETURNS TEXT /* & '_ str */ -STRICT STABLE PARALLEL SAFE -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'version_wrapper'; +-- crates/pg_warren/src/traversal_sql/walk.rs:38 +-- pg_warren::traversal_sql::walk::walk + +CREATE FUNCTION warren.walk(graph text, start anyarray, steps warren.step[], + mode text DEFAULT 'ends', distinct_ends boolean DEFAULT true, + max_results bigint DEFAULT NULL, reverse boolean DEFAULT false, + version bigint DEFAULT NULL) +RETURNS TABLE (end_id anyelement, path anyarray, rels text[], count bigint) +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'walk_wrapper'; /* */ /* */ --- crates/pg_warren/src/build/refresh.rs:20 --- pg_warren::build::refresh::refresh -CREATE FUNCTION warren."refresh"( +-- crates/pg_warren/src/catalog/promote.rs:14 +-- pg_warren::catalog::promote::promote +CREATE FUNCTION warren."promote"( "name" TEXT, /* & str */ - "promote" bool DEFAULT true, /* bool */ - "label" TEXT DEFAULT NULL /* Option < & str > */ -) RETURNS bigint /* i64 */ - + "version" bigint /* i64 */ +) RETURNS void +STRICT LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'refresh_wrapper'; +AS 'MODULE_PATHNAME', 'promote_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/constructors.rs:163 --- pg_warren::compute_sql::constructors::triangles -CREATE FUNCTION warren."triangles"( +-- crates/pg_warren/src/compute_sql/centrality.rs:125 +-- pg_warren::compute_sql::centrality::closeness_centrality +CREATE FUNCTION warren."closeness_centrality"( "graph" TEXT, /* Option < & str > */ "out" TEXT, /* Option < & str > */ + "k" bigint DEFAULT NULL, /* Option < i64 > */ + "seed" bigint DEFAULT 0, /* Option < i64 > */ + "wf_improved" bool DEFAULT true, /* Option < bool > */ + "directed" bool DEFAULT true, /* Option < bool > */ + "sources" TEXT DEFAULT NULL, /* Option < & str > */ "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ + "weighted" bool DEFAULT NULL, /* Option < bool > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ ) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'triangles_wrapper'; +AS 'MODULE_PATHNAME', 'closeness_centrality_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/from_query.rs:57 --- pg_warren::compute_sql::from_query::vector_from_query -CREATE FUNCTION warren."vector_from_query"( +-- crates/pg_warren/src/compute_sql/structure.rs:63 +-- pg_warren::compute_sql::structure::strong_components_vector +CREATE FUNCTION warren."strong_components_vector"( "graph" TEXT, /* Option < & str > */ - "name" TEXT, /* Option < & str > */ - "query" TEXT, /* Option < & str > */ - "dtype" TEXT DEFAULT 'f64', /* Option < & str > */ - "fill" double precision DEFAULT 0, /* Option < f64 > */ + "out" TEXT, /* Option < & str > */ + "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ ) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'vector_from_query_wrapper'; +AS 'MODULE_PATHNAME', 'strong_components_vector_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/map/mod.rs:91 --- pg_warren::compute_sql::map::vector_map -CREATE FUNCTION warren."vector_map"( +-- crates/pg_warren/src/community_sql/contract.rs:39 +-- pg_warren::community_sql::contract::contract +CREATE FUNCTION warren."contract"( "graph" TEXT, /* Option < & str > */ - "y" TEXT, /* Option < & str > */ - "expr" TEXT, /* Option < & str > */ - "persist" bool DEFAULT true, /* Option < bool > */ + "label_vector" TEXT, /* Option < & str > */ + "out_graph" TEXT, /* Option < & str > */ + "weighted" bool DEFAULT true, /* Option < bool > */ + "drop_self_loops" bool DEFAULT false, /* Option < bool > */ + "promote" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS void +) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'vector_map_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/cache/warm.rs:37 --- pg_warren::cache::warm::evict -CREATE FUNCTION warren."evict"( - "name" TEXT /* & str */ -) RETURNS bigint /* i64 */ -STRICT VOLATILE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'evict_wrapper'; +AS 'MODULE_PATHNAME', 'contract_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/gather/mod.rs:354 --- pg_warren::compute_sql::gather::gather +-- crates/pg_warren/src/catalog/owner/reassign.rs:19 +-- pg_warren::catalog::owner::reassign::reassign_owned -CREATE FUNCTION warren.gather(graph text, messages text, id_type anyelement, - value_type anycompatible, combine text DEFAULT 'collect', - direction text DEFAULT 'in', rels text[] DEFAULT NULL, - mask text DEFAULT NULL, mask_complement boolean DEFAULT false, - version bigint DEFAULT NULL, include_self boolean DEFAULT false) -RETURNS TABLE (node_id anyelement, value anycompatible) -VOLATILE PARALLEL UNSAFE LANGUAGE c AS 'MODULE_PATHNAME', 'gather_wrapper'; +CREATE FUNCTION warren."reassign_owned"("old" regrole, "new" regrole) RETURNS bigint +STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'reassign_owned_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/structure.rs:28 --- pg_warren::compute_sql::structure::core_number -CREATE FUNCTION warren."core_number"( +-- crates/pg_warren/src/compute_sql/constructors.rs:93 +-- pg_warren::compute_sql::constructors::vector_degree +CREATE FUNCTION warren."vector_degree"( "graph" TEXT, /* Option < & str > */ - "out" TEXT, /* Option < & str > */ + "name" TEXT, /* Option < & str > */ + "direction" TEXT DEFAULT 'out', /* Option < & str > */ "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ + "dtype" TEXT DEFAULT 'f64', /* Option < & str > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ ) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'core_number_wrapper'; +AS 'MODULE_PATHNAME', 'vector_degree_wrapper'; /* */ /* */ --- crates/pg_warren/src/spectral_sql/eigen.rs:91 --- pg_warren::spectral_sql::eigen::eigen -CREATE FUNCTION warren."eigen"( - "graph" TEXT, /* Option < & str > */ - "k" INT, /* Option < i32 > */ - "matrix" TEXT DEFAULT 'adjacency', /* Option < & str > */ - "which" TEXT DEFAULT 'largest', /* Option < & str > */ - "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ - "weighted" bool DEFAULT NULL, /* Option < bool > */ - "symmetrize" bool DEFAULT true, /* Option < bool > */ - "tol" double precision DEFAULT 1e-6, /* Option < f64 > */ - "max_iter" INT DEFAULT NULL, /* Option < i32 > */ - "vectors_prefix" TEXT DEFAULT NULL, /* Option < & str > */ - "version" bigint DEFAULT NULL, /* Option < i64 > */ - "dtype" TEXT DEFAULT 'f32', /* Option < & str > */ - "persist" bool DEFAULT true /* Option < bool > */ -) RETURNS TABLE ( - "i" INT, /* i32 */ - "eigenvalue" double precision, /* f64 */ - "residual" double precision /* f64 */ -) -VOLATILE PARALLEL UNSAFE +-- crates/pg_warren/src/traversal_sql/stats.rs:30 +-- pg_warren::traversal_sql::stats::last_stats +CREATE FUNCTION warren."last_stats"() RETURNS jsonb /* Option < JsonB > */ +STRICT VOLATILE PARALLEL RESTRICTED LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'eigen_wrapper'; +AS 'MODULE_PATHNAME', 'last_stats_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/product.rs:210 --- pg_warren::compute_sql::product::spmm -CREATE FUNCTION warren."spmm"( - "graph" TEXT, /* Option < & str > */ - "x" TEXT, /* Option < & str > */ - "y" TEXT, /* Option < & str > */ - "semiring" TEXT DEFAULT 'plus_times', /* Option < & str > */ - "transpose" bool DEFAULT false, /* Option < bool > */ - "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ - "weighted" bool DEFAULT NULL, /* Option < bool > */ - "mask" TEXT DEFAULT NULL, /* Option < & str > */ - "mask_complement" bool DEFAULT false, /* Option < bool > */ - "replace" bool DEFAULT false, /* Option < bool > */ - "accumulate" TEXT DEFAULT NULL, /* Option < & str > */ - "persist" bool DEFAULT true, /* Option < bool > */ - "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS void -VOLATILE PARALLEL UNSAFE -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'spmm_wrapper'; -/* */ +-- crates/pg_warren/src/catalog/dropped_roles/event_trigger.rs:21 +-- pg_warren::catalog::dropped_roles::event_trigger::on_drop_owned -/* */ --- crates/pg_warren/src/compute_sql/summary.rs:87 --- pg_warren::compute_sql::summary::vector_stats -CREATE FUNCTION warren."vector_stats"( - "graph" TEXT, /* Option < & str > */ - "name" TEXT, /* Option < & str > */ - "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS jsonb /* JsonB */ -STABLE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'vector_stats_wrapper'; +CREATE FUNCTION warren."on_drop_owned"() RETURNS event_trigger +VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'on_drop_owned_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/drop.rs:13 --- pg_warren::catalog::drop::drop_version -CREATE FUNCTION warren."drop_version"( - "name" TEXT, /* & str */ - "version" bigint /* i64 */ -) RETURNS void -STRICT -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'drop_version_wrapper'; -/* */ +-- crates/pg_warren/src/compute_sql/similarity.rs:48 +-- pg_warren::compute_sql::similarity::similarity -/* */ --- crates/pg_warren/src/version_sql/diff.rs:23 --- pg_warren::version_sql::diff::diff -CREATE FUNCTION warren."diff"( - "name" TEXT, /* & str */ - "v1" bigint, /* i64 */ - "v2" bigint /* i64 */ -) RETURNS jsonb /* JsonB */ -STRICT STABLE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'diff_wrapper'; +CREATE FUNCTION warren.similarity(graph text, pairs text, id_type anyelement, + measure text DEFAULT 'jaccard', rels text[] DEFAULT NULL, + version bigint DEFAULT NULL) +RETURNS TABLE (source anyelement, target anyelement, score float8) +STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'similarity_wrapper'; /* */ /* */ --- crates/pg_warren/src/community_sql/projection.rs:60 --- pg_warren::community_sql::projection::lift -CREATE FUNCTION warren."lift"( - "out_graph" TEXT, /* Option < & str > */ - "y" TEXT, /* Option < & str > */ +-- crates/pg_warren/src/compute_sql/centrality.rs:89 +-- pg_warren::compute_sql::centrality::betweenness_centrality +CREATE FUNCTION warren."betweenness_centrality"( "graph" TEXT, /* Option < & str > */ - "x" TEXT, /* Option < & str > */ + "out" TEXT, /* Option < & str > */ + "k" bigint DEFAULT NULL, /* Option < i64 > */ + "seed" bigint DEFAULT 0, /* Option < i64 > */ + "normalized" bool DEFAULT true, /* Option < bool > */ + "endpoints" bool DEFAULT false, /* Option < bool > */ + "directed" bool DEFAULT true, /* Option < bool > */ + "sources" TEXT DEFAULT NULL, /* Option < & str > */ + "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ + "weighted" bool DEFAULT NULL, /* Option < bool > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS void +) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'lift_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/compute_sql/reduce.rs:47 --- pg_warren::compute_sql::reduce::vector_norm_diff -CREATE FUNCTION warren."vector_norm_diff"( - "graph" TEXT, /* Option < & str > */ - "a" TEXT, /* Option < & str > */ - "b" TEXT, /* Option < & str > */ - "norm" TEXT DEFAULT 'l2', /* Option < & str > */ - "a_version" bigint DEFAULT NULL, /* Option < i64 > */ - "b_version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS double precision /* f64 */ -STABLE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'vector_norm_diff_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/catalog/info.rs:15 --- pg_warren::catalog::info::info -CREATE FUNCTION warren."info"( - "name" TEXT /* & str */ -) RETURNS jsonb /* JsonB */ -STRICT STABLE PARALLEL SAFE -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'info_wrapper'; +AS 'MODULE_PATHNAME', 'betweenness_centrality_wrapper'; /* */ - -/* */ --- crates/pg_warren/src/catalog/promote.rs:14 --- pg_warren::catalog::promote::promote -CREATE FUNCTION warren."promote"( - "name" TEXT, /* & str */ - "version" bigint /* i64 */ -) RETURNS void -STRICT -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'promote_wrapper'; + +/* */ +-- crates/pg_warren/src/compute_sql/read.rs:98 +-- pg_warren::compute_sql::read::matrix_rows + +CREATE FUNCTION warren.matrix_rows(graph text, name text, id_type anyelement, + version bigint DEFAULT NULL) +RETURNS TABLE (node_id anyelement, "row" float8[]) +STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'matrix_rows_wrapper'; /* */ /* */ @@ -1668,309 +1715,299 @@ AS 'MODULE_PATHNAME', 'topological_order_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/map/mod.rs:156 --- pg_warren::compute_sql::map::explain_map -CREATE FUNCTION warren."explain_map"( - "graph" TEXT, /* Option < & str > */ - "expr" TEXT, /* Option < & str > */ - "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS jsonb /* JsonB */ -STABLE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'explain_map_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/compute_sql/product.rs:172 --- pg_warren::compute_sql::product::spmv -CREATE FUNCTION warren."spmv"( +-- crates/pg_warren/src/compute_sql/centrality.rs:157 +-- pg_warren::compute_sql::centrality::harmonic_centrality +CREATE FUNCTION warren."harmonic_centrality"( "graph" TEXT, /* Option < & str > */ - "x" TEXT, /* Option < & str > */ - "y" TEXT, /* Option < & str > */ - "semiring" TEXT DEFAULT 'plus_times', /* Option < & str > */ - "transpose" bool DEFAULT false, /* Option < bool > */ + "out" TEXT, /* Option < & str > */ + "k" bigint DEFAULT NULL, /* Option < i64 > */ + "seed" bigint DEFAULT 0, /* Option < i64 > */ + "directed" bool DEFAULT true, /* Option < bool > */ + "sources" TEXT DEFAULT NULL, /* Option < & str > */ "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ "weighted" bool DEFAULT NULL, /* Option < bool > */ - "mask" TEXT DEFAULT NULL, /* Option < & str > */ - "mask_complement" bool DEFAULT false, /* Option < bool > */ - "replace" bool DEFAULT false, /* Option < bool > */ - "accumulate" TEXT DEFAULT NULL, /* Option < & str > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS void +) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'spmv_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/compute_sql/reduce.rs:19 --- pg_warren::compute_sql::reduce::vector_reduce -CREATE FUNCTION warren."vector_reduce"( - "graph" TEXT, /* Option < & str > */ - "x" TEXT, /* Option < & str > */ - "op" TEXT, /* Option < & str > */ - "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS double precision /* Option < f64 > */ -STABLE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'vector_reduce_wrapper'; +AS 'MODULE_PATHNAME', 'harmonic_centrality_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/paths.rs:114 --- pg_warren::traversal_sql::paths::dijkstra +-- crates/pg_warren/src/catalog/dropped_roles/drop_owned.rs:15 +-- pg_warren::catalog::dropped_roles::drop_owned::drop_owned -CREATE FUNCTION warren.dijkstra(graph text, start anyelement, target anyelement, - rels text[] DEFAULT NULL, direction text DEFAULT 'out', - version bigint DEFAULT NULL) -RETURNS TABLE (path anyarray, cost float8) -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'dijkstra_wrapper'; +CREATE FUNCTION warren."drop_owned"(role regrole) RETURNS bigint +STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'drop_owned_wrapper'; /* */ /* */ --- crates/pg_warren/src/spectral_sql/distance/mod.rs:96 --- pg_warren::spectral_sql::distance::distance -CREATE FUNCTION warren."distance"( +-- crates/pg_warren/src/spectral_sql/eigen.rs:91 +-- pg_warren::spectral_sql::eigen::eigen +CREATE FUNCTION warren."eigen"( "graph" TEXT, /* Option < & str > */ - "v1" bigint, /* Option < i64 > */ - "v2" bigint, /* Option < i64 > */ - "method" TEXT, /* Option < & str > */ - "vectors_prefix" TEXT DEFAULT NULL, /* Option < & str > */ - "k" INT DEFAULT NULL, /* Option < i32 > */ - "x" TEXT DEFAULT NULL, /* Option < & str > */ - "y" TEXT DEFAULT NULL, /* Option < & str > */ - "compared_rows" TEXT DEFAULT 'shared', /* Option < & str > */ - "matrix" TEXT DEFAULT NULL, /* Option < & str > */ + "k" INT, /* Option < i32 > */ + "matrix" TEXT DEFAULT 'adjacency', /* Option < & str > */ "which" TEXT DEFAULT 'largest', /* Option < & str > */ "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ "weighted" bool DEFAULT NULL, /* Option < bool > */ "symmetrize" bool DEFAULT true, /* Option < bool > */ "tol" double precision DEFAULT 1e-6, /* Option < f64 > */ "max_iter" INT DEFAULT NULL, /* Option < i32 > */ - "dtype" TEXT DEFAULT 'f32', /* Option < & str > */ - "graph2" TEXT DEFAULT NULL /* Option < & str > */ -) RETURNS jsonb /* JsonB */ -STABLE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'distance_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/compute_sql/drop.rs:16 --- pg_warren::compute_sql::drop::drop_vector -CREATE FUNCTION warren."drop_vector"( - "graph" TEXT, /* Option < & str > */ - "name" TEXT, /* Option < & str > */ + "vectors_prefix" TEXT DEFAULT NULL, /* Option < & str > */ "version" bigint DEFAULT NULL, /* Option < i64 > */ - "if_exists" bool DEFAULT false /* Option < bool > */ -) RETURNS void + "dtype" TEXT DEFAULT 'f32', /* Option < & str > */ + "persist" bool DEFAULT true /* Option < bool > */ +) RETURNS TABLE ( + "i" INT, /* i32 */ + "eigenvalue" double precision, /* f64 */ + "residual" double precision /* f64 */ +) VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'drop_vector_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/compute_sql/summary.rs:20 --- pg_warren::compute_sql::summary::vector_topk - -CREATE FUNCTION warren.vector_topk(graph text, name text, k int, id_type anyelement, - descending bool DEFAULT true, version bigint DEFAULT NULL) -RETURNS TABLE (node_id anyelement, value float8) -STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'vector_topk_wrapper'; +AS 'MODULE_PATHNAME', 'eigen_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/step.rs:93 --- pg_warren::traversal_sql::step::s +-- crates/pg_warren/src/traversal_sql/bfs.rs:37 +-- pg_warren::traversal_sql::bfs::bfs -CREATE FUNCTION warren.s(rels text[], direction text DEFAULT 'out', filter "any" DEFAULT NULL, - min_hops int DEFAULT 1, max_hops int DEFAULT NULL) -RETURNS warren.step -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 's_wrapper'; +CREATE FUNCTION warren.bfs(graph text, start anyarray, max_depth int, + direction text DEFAULT 'out', rels text[] DEFAULT NULL, + node_filter "any" DEFAULT NULL, max_results bigint DEFAULT NULL, + version bigint DEFAULT NULL) +RETURNS TABLE (node_id anyelement, depth int, via anyelement) +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'bfs_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/structure.rs:63 --- pg_warren::compute_sql::structure::strong_components_vector -CREATE FUNCTION warren."strong_components_vector"( +-- crates/pg_warren/src/community_sql/local_moving.rs:96 +-- pg_warren::community_sql::local_moving::leiden_refine +CREATE FUNCTION warren."leiden_refine"( "graph" TEXT, /* Option < & str > */ + "communities" TEXT, /* Option < & str > */ "out" TEXT, /* Option < & str > */ - "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ + "keys" TEXT DEFAULT NULL, /* Option < & str > */ + "resolution" double precision DEFAULT 1.0, /* Option < f64 > */ + "seed" bigint DEFAULT 0, /* Option < i64 > */ + "randomness" double precision DEFAULT 0.01, /* Option < f64 > */ + "weighted" bool DEFAULT NULL, /* Option < bool > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ ) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'strong_components_vector_wrapper'; +AS 'MODULE_PATHNAME', 'leiden_refine_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/walk.rs:38 --- pg_warren::traversal_sql::walk::walk +-- crates/pg_warren/src/catalog/drop.rs:13 +-- pg_warren::catalog::drop::drop_version +CREATE FUNCTION warren."drop_version"( + "name" TEXT, /* & str */ + "version" bigint /* i64 */ +) RETURNS void +STRICT +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'drop_version_wrapper'; +/* */ -CREATE FUNCTION warren.walk(graph text, start anyarray, steps warren.step[], - mode text DEFAULT 'ends', distinct_ends boolean DEFAULT true, - max_results bigint DEFAULT NULL, reverse boolean DEFAULT false, - version bigint DEFAULT NULL) -RETURNS TABLE (end_id anyelement, path anyarray, rels text[], count bigint) -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'walk_wrapper'; +/* */ +-- crates/pg_warren/src/compute_sql/product.rs:172 +-- pg_warren::compute_sql::product::spmv +CREATE FUNCTION warren."spmv"( + "graph" TEXT, /* Option < & str > */ + "x" TEXT, /* Option < & str > */ + "y" TEXT, /* Option < & str > */ + "semiring" TEXT DEFAULT 'plus_times', /* Option < & str > */ + "transpose" bool DEFAULT false, /* Option < bool > */ + "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ + "weighted" bool DEFAULT NULL, /* Option < bool > */ + "mask" TEXT DEFAULT NULL, /* Option < & str > */ + "mask_complement" bool DEFAULT false, /* Option < bool > */ + "replace" bool DEFAULT false, /* Option < bool > */ + "accumulate" TEXT DEFAULT NULL, /* Option < & str > */ + "persist" bool DEFAULT true, /* Option < bool > */ + "version" bigint DEFAULT NULL /* Option < i64 > */ +) RETURNS void +VOLATILE PARALLEL UNSAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'spmv_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/similarity.rs:179 --- pg_warren::compute_sql::similarity::most_similar +-- crates/pg_warren/src/compute_sql/read.rs:62 +-- pg_warren::compute_sql::read::vector_values -CREATE FUNCTION warren.most_similar(graph text, node anyelement, k bigint DEFAULT 10, - measure text DEFAULT 'jaccard', rels text[] DEFAULT NULL, - version bigint DEFAULT NULL) -RETURNS TABLE (node_id anyelement, score float8) -STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'most_similar_wrapper'; +CREATE FUNCTION warren.vector_values(graph text, name text, id_type anyelement, + version bigint DEFAULT NULL) +RETURNS TABLE (node_id anyelement, value float8) +STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'vector_values_wrapper'; /* */ /* */ --- crates/pg_warren/src/cache/warm.rs:18 --- pg_warren::cache::warm::prewarm -CREATE FUNCTION warren."prewarm"( - "name" TEXT, /* & str */ - "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS bigint /* i64 */ -VOLATILE PARALLEL RESTRICTED -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'prewarm_wrapper'; +-- crates/pg_warren/src/catalog/readers.rs:25 +-- pg_warren::catalog::readers::revoke + +CREATE FUNCTION warren."revoke"(name text, role regrole) RETURNS void +STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'revoke_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/drop.rs:28 --- pg_warren::catalog::drop::drop_graph -CREATE FUNCTION warren."drop_graph"( +-- crates/pg_warren/src/cache/warm.rs:37 +-- pg_warren::cache::warm::evict +CREATE FUNCTION warren."evict"( "name" TEXT /* & str */ -) RETURNS void -STRICT +) RETURNS bigint /* i64 */ +STRICT VOLATILE PARALLEL RESTRICTED LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'drop_graph_wrapper'; +AS 'MODULE_PATHNAME', 'evict_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/read.rs:132 --- pg_warren::compute_sql::read::id_map +-- crates/pg_warren/src/traversal_sql/step.rs:93 +-- pg_warren::traversal_sql::step::s -CREATE FUNCTION warren.id_map(graph text, id_type anyelement, version bigint DEFAULT NULL) -RETURNS TABLE (node_id anyelement, dense_id bigint, live boolean) -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'id_map_wrapper'; +CREATE FUNCTION warren.s(rels text[], direction text DEFAULT 'out', filter "any" DEFAULT NULL, + min_hops int DEFAULT 1, max_hops int DEFAULT NULL) +RETURNS warren.step +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 's_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/centrality.rs:89 --- pg_warren::compute_sql::centrality::betweenness_centrality -CREATE FUNCTION warren."betweenness_centrality"( +-- crates/pg_warren/src/community_sql/modularity.rs:26 +-- pg_warren::community_sql::modularity::modularity +CREATE FUNCTION warren."modularity"( "graph" TEXT, /* Option < & str > */ - "out" TEXT, /* Option < & str > */ - "k" bigint DEFAULT NULL, /* Option < i64 > */ - "seed" bigint DEFAULT 0, /* Option < i64 > */ - "normalized" bool DEFAULT true, /* Option < bool > */ - "endpoints" bool DEFAULT false, /* Option < bool > */ - "directed" bool DEFAULT true, /* Option < bool > */ - "sources" TEXT DEFAULT NULL, /* Option < & str > */ - "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ + "labels" TEXT, /* Option < & str > */ + "resolution" double precision DEFAULT 1.0, /* Option < f64 > */ "weighted" bool DEFAULT NULL, /* Option < bool > */ - "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS bigint /* Option < i64 > */ -VOLATILE PARALLEL UNSAFE +) RETURNS double precision /* Option < f64 > */ +STABLE PARALLEL RESTRICTED LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'betweenness_centrality_wrapper'; +AS 'MODULE_PATHNAME', 'modularity_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/bfs.rs:37 --- pg_warren::traversal_sql::bfs::bfs +-- crates/pg_warren/src/traversal_sql/paths.rs:69 +-- pg_warren::traversal_sql::paths::shortest_path -CREATE FUNCTION warren.bfs(graph text, start anyarray, max_depth int, - direction text DEFAULT 'out', rels text[] DEFAULT NULL, - node_filter "any" DEFAULT NULL, max_results bigint DEFAULT NULL, - version bigint DEFAULT NULL) -RETURNS TABLE (node_id anyelement, depth int, via anyelement) -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'bfs_wrapper'; +CREATE FUNCTION warren.shortest_path(graph text, start anyelement, target anyelement, + max_depth int DEFAULT NULL, rels text[] DEFAULT NULL, + direction text DEFAULT 'out', version bigint DEFAULT NULL) +RETURNS anyarray +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'shortest_path_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/centrality.rs:157 --- pg_warren::compute_sql::centrality::harmonic_centrality -CREATE FUNCTION warren."harmonic_centrality"( +-- crates/pg_warren/src/compute_sql/product.rs:210 +-- pg_warren::compute_sql::product::spmm +CREATE FUNCTION warren."spmm"( "graph" TEXT, /* Option < & str > */ - "out" TEXT, /* Option < & str > */ - "k" bigint DEFAULT NULL, /* Option < i64 > */ - "seed" bigint DEFAULT 0, /* Option < i64 > */ - "directed" bool DEFAULT true, /* Option < bool > */ - "sources" TEXT DEFAULT NULL, /* Option < & str > */ + "x" TEXT, /* Option < & str > */ + "y" TEXT, /* Option < & str > */ + "semiring" TEXT DEFAULT 'plus_times', /* Option < & str > */ + "transpose" bool DEFAULT false, /* Option < bool > */ "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ "weighted" bool DEFAULT NULL, /* Option < bool > */ + "mask" TEXT DEFAULT NULL, /* Option < & str > */ + "mask_complement" bool DEFAULT false, /* Option < bool > */ + "replace" bool DEFAULT false, /* Option < bool > */ + "accumulate" TEXT DEFAULT NULL, /* Option < & str > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS bigint /* Option < i64 > */ +) RETURNS void VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'harmonic_centrality_wrapper'; +AS 'MODULE_PATHNAME', 'spmm_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/read.rs:98 --- pg_warren::compute_sql::read::matrix_rows +-- crates/pg_warren/src/catalog/owner/mod.rs:27 +-- pg_warren::catalog::owner::set_owner -CREATE FUNCTION warren.matrix_rows(graph text, name text, id_type anyelement, - version bigint DEFAULT NULL) -RETURNS TABLE (node_id anyelement, "row" float8[]) -STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'matrix_rows_wrapper'; +CREATE FUNCTION warren."set_owner"(name text, owner regrole) RETURNS void +STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'set_owner_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/owner/mod.rs:25 --- pg_warren::catalog::owner::set_owner +-- crates/pg_warren/src/traversal_sql/neighbourhood.rs:23 +-- pg_warren::traversal_sql::neighbourhood::neighbours -CREATE FUNCTION warren."set_owner"(name text, owner regrole) RETURNS void -STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'set_owner_wrapper'; +CREATE FUNCTION warren.neighbours(graph text, node anyelement, direction text DEFAULT 'out', + rels text[] DEFAULT NULL, version bigint DEFAULT NULL) +RETURNS SETOF anyelement +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'neighbours_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/dropped_roles/orphans.rs:33 --- pg_warren::catalog::dropped_roles::orphans::orphans -CREATE FUNCTION warren."orphans"() RETURNS TABLE ( - "kind" TEXT, /* String */ - "graph" TEXT, /* String */ - "role" oid /* pg_sys :: Oid */ -) -STRICT VOLATILE PARALLEL SAFE +-- crates/pg_warren/src/compute_sql/map/mod.rs:91 +-- pg_warren::compute_sql::map::vector_map +CREATE FUNCTION warren."vector_map"( + "graph" TEXT, /* Option < & str > */ + "y" TEXT, /* Option < & str > */ + "expr" TEXT, /* Option < & str > */ + "persist" bool DEFAULT true, /* Option < bool > */ + "version" bigint DEFAULT NULL /* Option < i64 > */ +) RETURNS void +VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'orphans_wrapper'; +AS 'MODULE_PATHNAME', 'vector_map_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/dropped_roles/event_trigger.rs:21 --- pg_warren::catalog::dropped_roles::event_trigger::on_drop_owned - -CREATE FUNCTION warren."on_drop_owned"() RETURNS event_trigger -VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'on_drop_owned_wrapper'; +-- crates/pg_warren/src/compute_sql/drop.rs:16 +-- pg_warren::compute_sql::drop::drop_vector +CREATE FUNCTION warren."drop_vector"( + "graph" TEXT, /* Option < & str > */ + "name" TEXT, /* Option < & str > */ + "version" bigint DEFAULT NULL, /* Option < i64 > */ + "if_exists" bool DEFAULT false /* Option < bool > */ +) RETURNS void +VOLATILE PARALLEL UNSAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'drop_vector_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/dropped_roles/drop_owned.rs:15 --- pg_warren::catalog::dropped_roles::drop_owned::drop_owned - -CREATE FUNCTION warren."drop_owned"(role regrole) RETURNS bigint -STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'drop_owned_wrapper'; +-- crates/pg_warren/src/catalog/info.rs:16 +-- pg_warren::catalog::info::info +CREATE FUNCTION warren."info"( + "name" TEXT /* & str */ +) RETURNS jsonb /* JsonB */ +STRICT STABLE PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'info_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/owner/reassign.rs:19 --- pg_warren::catalog::owner::reassign::reassign_owned +-- crates/pg_warren/src/version_sql/diff.rs:23 +-- pg_warren::version_sql::diff::diff +CREATE FUNCTION warren."diff"( + "name" TEXT, /* & str */ + "v1" bigint, /* i64 */ + "v2" bigint /* i64 */ +) RETURNS jsonb /* JsonB */ +STRICT STABLE PARALLEL RESTRICTED +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'diff_wrapper'; +/* */ -CREATE FUNCTION warren."reassign_owned"("old" regrole, "new" regrole) RETURNS bigint -STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'reassign_owned_wrapper'; +/* */ +-- crates/pg_warren/src/catalog/drop.rs:29 +-- pg_warren::catalog::drop::drop_graph +CREATE FUNCTION warren."drop_graph"( + "name" TEXT /* & str */ +) RETURNS void +STRICT +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'drop_graph_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/dropped_roles/orphans.rs:69 +-- crates/pg_warren/src/catalog/dropped_roles/orphans.rs:70 -- pg_warren::catalog::dropped_roles::orphans::drop_orphans CREATE FUNCTION warren."drop_orphans"() RETURNS bigint /* i64 */ STRICT @@ -1979,120 +2016,100 @@ AS 'MODULE_PATHNAME', 'drop_orphans_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/readers.rs:25 --- pg_warren::catalog::readers::revoke - -CREATE FUNCTION warren."revoke"(name text, role regrole) RETURNS void -STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'revoke_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/traversal_sql/neighbourhood.rs:81 --- pg_warren::traversal_sql::neighbourhood::has_edge +-- crates/pg_warren/src/compute_sql/similarity.rs:179 +-- pg_warren::compute_sql::similarity::most_similar -CREATE FUNCTION warren.has_edge(graph text, src anyelement, dst anyelement, - rel text DEFAULT NULL, version bigint DEFAULT NULL) -RETURNS boolean -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'has_edge_wrapper'; +CREATE FUNCTION warren.most_similar(graph text, node anyelement, k bigint DEFAULT 10, + measure text DEFAULT 'jaccard', rels text[] DEFAULT NULL, + version bigint DEFAULT NULL) +RETURNS TABLE (node_id anyelement, score float8) +STABLE PARALLEL RESTRICTED LANGUAGE c AS 'MODULE_PATHNAME', 'most_similar_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/structure.rs:139 --- pg_warren::compute_sql::structure::dag_longest_path -CREATE FUNCTION warren."dag_longest_path"( - "graph" TEXT, /* Option < & str > */ - "out" TEXT, /* Option < & str > */ - "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ - "weighted" bool DEFAULT NULL, /* Option < bool > */ - "persist" bool DEFAULT true, /* Option < bool > */ +-- crates/pg_warren/src/cache/warm.rs:18 +-- pg_warren::cache::warm::prewarm +CREATE FUNCTION warren."prewarm"( + "name" TEXT, /* & str */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS double precision /* Option < f64 > */ -VOLATILE PARALLEL UNSAFE +) RETURNS bigint /* i64 */ +VOLATILE PARALLEL RESTRICTED LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'dag_longest_path_wrapper'; +AS 'MODULE_PATHNAME', 'prewarm_wrapper'; /* */ /* */ --- crates/pg_warren/src/community_sql/projection.rs:26 --- pg_warren::community_sql::projection::project -CREATE FUNCTION warren."project"( +-- crates/pg_warren/src/compute_sql/structure.rs:28 +-- pg_warren::compute_sql::structure::core_number +CREATE FUNCTION warren."core_number"( "graph" TEXT, /* Option < & str > */ - "x" TEXT, /* Option < & str > */ - "out_graph" TEXT, /* Option < & str > */ - "y" TEXT, /* Option < & str > */ - "agg" TEXT DEFAULT 'sum', /* Option < & str > */ + "out" TEXT, /* Option < & str > */ + "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ -) RETURNS void +) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'project_wrapper'; +AS 'MODULE_PATHNAME', 'core_number_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/neighbourhood.rs:52 --- pg_warren::traversal_sql::neighbourhood::degree +-- crates/pg_warren/src/vector/quota/sql.rs:18 +-- pg_warren::vector::quota::sql::set_vector_quota -CREATE FUNCTION warren.degree(graph text, node anyelement, direction text DEFAULT 'out', - rels text[] DEFAULT NULL, version bigint DEFAULT NULL) -RETURNS bigint -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'degree_wrapper'; +CREATE FUNCTION warren."set_vector_quota"(name text, bytes bigint, role regrole DEFAULT NULL) +RETURNS void VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'set_vector_quota_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/bfs.rs:87 --- pg_warren::traversal_sql::bfs::reachable_set +-- crates/pg_warren/src/traversal_sql/neighbourhood.rs:81 +-- pg_warren::traversal_sql::neighbourhood::has_edge -CREATE FUNCTION warren.reachable_set(graph text, start anyarray, max_depth int DEFAULT NULL, - rels text[] DEFAULT NULL, direction text DEFAULT 'out', - version bigint DEFAULT NULL) -RETURNS SETOF anyelement -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'reachable_set_wrapper'; +CREATE FUNCTION warren.has_edge(graph text, src anyelement, dst anyelement, + rel text DEFAULT NULL, version bigint DEFAULT NULL) +RETURNS boolean +STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'has_edge_wrapper'; /* */ /* */ --- crates/pg_warren/src/traversal_sql/paths.rs:25 --- pg_warren::traversal_sql::paths::reachable - -CREATE FUNCTION warren.reachable(graph text, start anyelement, target anyelement, - max_depth int DEFAULT NULL, rels text[] DEFAULT NULL, - direction text DEFAULT 'out', version bigint DEFAULT NULL) -RETURNS boolean -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'reachable_wrapper'; +-- crates/pg_warren/src/compute_sql/constructors.rs:38 +-- pg_warren::compute_sql::constructors::vector_const +CREATE FUNCTION warren."vector_const"( + "graph" TEXT, /* Option < & str > */ + "name" TEXT, /* Option < & str > */ + "value" double precision, /* Option < f64 > */ + "dtype" TEXT DEFAULT 'f64', /* Option < & str > */ + "persist" bool DEFAULT true, /* Option < bool > */ + "version" bigint DEFAULT NULL /* Option < i64 > */ +) RETURNS bigint /* Option < i64 > */ +VOLATILE PARALLEL UNSAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'vector_const_wrapper'; /* */ /* */ --- crates/pg_warren/src/compute_sql/centrality.rs:125 --- pg_warren::compute_sql::centrality::closeness_centrality -CREATE FUNCTION warren."closeness_centrality"( +-- crates/pg_warren/src/community_sql/local_moving.rs:29 +-- pg_warren::community_sql::local_moving::local_moving +CREATE FUNCTION warren."local_moving"( "graph" TEXT, /* Option < & str > */ "out" TEXT, /* Option < & str > */ - "k" bigint DEFAULT NULL, /* Option < i64 > */ + "initial" TEXT DEFAULT NULL, /* Option < & str > */ + "keys" TEXT DEFAULT NULL, /* Option < & str > */ + "resolution" double precision DEFAULT 1.0, /* Option < f64 > */ "seed" bigint DEFAULT 0, /* Option < i64 > */ - "wf_improved" bool DEFAULT true, /* Option < bool > */ - "directed" bool DEFAULT true, /* Option < bool > */ - "sources" TEXT DEFAULT NULL, /* Option < & str > */ - "rels" TEXT[] DEFAULT NULL, /* :: std :: option :: Option < Vec < Option < String > > > */ + "max_sweeps" INT DEFAULT 100, /* Option < i32 > */ + "tolerance" double precision DEFAULT 1e-7, /* Option < f64 > */ "weighted" bool DEFAULT NULL, /* Option < bool > */ "persist" bool DEFAULT true, /* Option < bool > */ "version" bigint DEFAULT NULL /* Option < i64 > */ ) RETURNS bigint /* Option < i64 > */ VOLATILE PARALLEL UNSAFE LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'closeness_centrality_wrapper'; -/* */ - -/* */ --- crates/pg_warren/src/traversal_sql/components.rs:21 --- pg_warren::traversal_sql::components::components - -CREATE FUNCTION warren.components(graph text, id_type anyelement, version bigint DEFAULT NULL) -RETURNS TABLE (node_id anyelement, component_id bigint) -STABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'components_wrapper'; +AS 'MODULE_PATHNAME', 'local_moving_wrapper'; /* */ /* */ --- crates/pg_warren/src/catalog/bootstrap.rs:7 +-- crates/pg_warren/src/catalog/bootstrap.rs:8 -- finalize CREATE TABLE warren.graphs ( @@ -2114,7 +2131,8 @@ CREATE TABLE warren.graphs ( current_version bigint, store_merged boolean NOT NULL DEFAULT true, node_order text NOT NULL DEFAULT 'id' - CHECK (node_order IN ('id'::pg_catalog.text, 'locality'::pg_catalog.text)) + CHECK (node_order IN ('id'::pg_catalog.text, 'locality'::pg_catalog.text)), + reader_vector_quota bigint CHECK (reader_vector_quota >= 0) ); CREATE TABLE warren.versions ( @@ -2168,6 +2186,17 @@ CREATE TABLE warren.vector_chunks ( ); -- IEEE floats rarely compress, so skip pglz's attempt on every write. ALTER TABLE warren.vector_chunks ALTER COLUMN data SET STORAGE EXTERNAL; +-- A quota sums one role's vectors on one graph (DESIGN.md §5.7). +CREATE INDEX vectors_created_by ON warren.vectors (graph_id, created_by); + +-- Per-role quotas on readers' persisted vectors, overriding +-- graphs.reader_vector_quota (DESIGN.md §5.7, §14 item 46). +CREATE TABLE warren.vector_quotas ( + graph_id int NOT NULL REFERENCES warren.graphs ON DELETE CASCADE, + role regrole NOT NULL, + bytes bigint NOT NULL CHECK (bytes >= 0), + PRIMARY KEY (graph_id, role) +); -- graphs.current_version is not a foreign key: with versions.graph_id it -- would make a cycle, and pg_dump cannot order the data of tables in a @@ -2253,5 +2282,6 @@ SELECT pg_catalog.pg_extension_config_dump('warren.segments', ''); SELECT pg_catalog.pg_extension_config_dump('warren.vectors', ''); SELECT pg_catalog.pg_extension_config_dump('warren.vectors_vector_id_seq', ''); SELECT pg_catalog.pg_extension_config_dump('warren.vector_chunks', ''); +SELECT pg_catalog.pg_extension_config_dump('warren.vector_quotas', ''); /* */ From 0404bfa9c0a29194da844ef3485f17063a394556 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 16:01:57 +0000 Subject: [PATCH 2/7] Dropped roles: remove, list and sweep quota rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clean_up_after (drop_owned and the DROP OWNED trigger) deletes the role's rows of warren.vector_quotas, orphans() lists rows naming dropped roles as kind 'quota', and drop_orphans removes them in a fourth locked pass, at the seams step 6 left (DESIGN.md §5.7). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke --- .../src/catalog/dropped_roles/drop_owned.rs | 5 +- .../catalog/dropped_roles/event_trigger.rs | 6 ++- .../src/catalog/dropped_roles/mod.rs | 7 ++- .../src/catalog/dropped_roles/orphans.rs | 52 +++++++++++++------ crates/pg_warren/src/fixture.rs | 18 +++++-- 5 files changed, 60 insertions(+), 28 deletions(-) diff --git a/crates/pg_warren/src/catalog/dropped_roles/drop_owned.rs b/crates/pg_warren/src/catalog/dropped_roles/drop_owned.rs index d746c058..29367018 100644 --- a/crates/pg_warren/src/catalog/dropped_roles/drop_owned.rs +++ b/crates/pg_warren/src/catalog/dropped_roles/drop_owned.rs @@ -52,14 +52,15 @@ mod tests { use pgrx::prelude::*; use pgrx::spi::Error; - use crate::fixture::{failure, failure_as, neighbouring_graphs, ownership}; + use crate::fixture::{failure, failure_as, neighbouring_graphs, ownership, quotas}; #[pg_test] - fn drop_owned_drops_the_roles_graphs_its_vectors_and_its_reader_entries() -> Result<(), Error> { + fn drop_owned_drops_the_roles_graphs_vectors_reader_entries_and_quotas() -> Result<(), Error> { neighbouring_graphs()?; let dropped = Spi::get_one::("SELECT warren.drop_owned('dr_owner')")?; assert_eq!(dropped, Some(1)); assert_eq!(ownership()?.as_deref(), Some("dk:dk_owner:{} / dk.theirs:dk_owner")); + assert_eq!(quotas()?.as_deref(), Some("")); Ok(()) } diff --git a/crates/pg_warren/src/catalog/dropped_roles/event_trigger.rs b/crates/pg_warren/src/catalog/dropped_roles/event_trigger.rs index aa34bd99..ad75c21c 100644 --- a/crates/pg_warren/src/catalog/dropped_roles/event_trigger.rs +++ b/crates/pg_warren/src/catalog/dropped_roles/event_trigger.rs @@ -131,7 +131,7 @@ mod tests { use pgrx::prelude::*; use pgrx::spi::Error; - use crate::fixture::{failure, neighbouring_graphs, ownership, statement_failure_as}; + use crate::fixture::{failure, neighbouring_graphs, ownership, quotas, statement_failure_as}; /// The trigger as `docs/semantics.md` tells a superuser to create it. fn install() -> Result<(), Error> { @@ -187,7 +187,8 @@ mod tests { } #[pg_test] - fn after_reassign_owned_drop_owned_removes_the_roles_reader_entries() -> Result<(), Error> { + fn after_reassign_owned_drop_owned_removes_the_roles_reader_entries_and_quotas( + ) -> Result<(), Error> { neighbouring_graphs()?; install()?; Spi::run( @@ -199,6 +200,7 @@ mod tests { ownership()?.as_deref(), Some("dk:dk_owner:{} dr:dr_heir:{} / dk.mine:dr_heir dk.theirs:dk_owner") ); + assert_eq!(quotas()?.as_deref(), Some("")); Ok(()) } diff --git a/crates/pg_warren/src/catalog/dropped_roles/mod.rs b/crates/pg_warren/src/catalog/dropped_roles/mod.rs index 682fef34..662c3ff4 100644 --- a/crates/pg_warren/src/catalog/dropped_roles/mod.rs +++ b/crates/pg_warren/src/catalog/dropped_roles/mod.rs @@ -16,8 +16,8 @@ use pgrx::spi::SpiClient; /// Removes what `DROP OWNED` removes of `role` besides what it owns: the /// vectors it wrote on other roles' graphs (those on its own graphs go -/// with them) and its entries in every graph's readers. `client` must run -/// as the catalog owner. +/// with them), its entries in every graph's readers and its quotas on +/// readers' vectors. `client` must run as the catalog owner. fn clean_up_after(client: &mut SpiClient<'_>, role: pg_sys::Oid) -> Result<(), pgrx::spi::Error> { client.update("DELETE FROM warren.vectors WHERE created_by::oid = $1", None, &[role.into()])?; client.update( @@ -28,7 +28,6 @@ fn clean_up_after(client: &mut SpiClient<'_>, role: pg_sys::Oid) -> Result<(), p None, &[role.into()], )?; - // Quotas (work breakdown step 7) add the role's rows of - // `warren.vector_quotas` here (DESIGN.md §5.7, `drop_owned`). + client.update("DELETE FROM warren.vector_quotas WHERE role::oid = $1", None, &[role.into()])?; Ok(()) } diff --git a/crates/pg_warren/src/catalog/dropped_roles/orphans.rs b/crates/pg_warren/src/catalog/dropped_roles/orphans.rs index 240c530a..21f12591 100644 --- a/crates/pg_warren/src/catalog/dropped_roles/orphans.rs +++ b/crates/pg_warren/src/catalog/dropped_roles/orphans.rs @@ -8,13 +8,16 @@ use super::super::CatalogError; use crate::access::{as_catalog_owner, entry, is_owner, may_drop_orphans, require_orphan_dropper}; /// Every reference to a dropped role, as `(kind, graph, role, owner)`: -/// graphs whose owner was dropped, reader entries, and (once per graph and -/// role) vectors whose creator was. Quotas (work breakdown step 7) add -/// their rows of `warren.vector_quotas` here (DESIGN.md §5.7). +/// graphs whose owner was dropped, quotas on readers' vectors, reader +/// entries, and (once per graph and role) vectors whose creator was. const ORPHANS: &str = " SELECT 'graph', g.name, g.owner::oid, g.owner::oid FROM warren.graphs g WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles r WHERE r.oid = g.owner::oid) UNION ALL + SELECT 'quota', g.name, q.role::oid, g.owner::oid + FROM warren.vector_quotas q JOIN warren.graphs g USING (graph_id) + WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles r WHERE r.oid = q.role::oid) + UNION ALL SELECT DISTINCT 'reader', g.name, x.reader::oid, g.owner::oid FROM warren.graphs g, pg_catalog.unnest(g.readers) AS x (reader) WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles r WHERE r.oid = x.reader::oid) @@ -24,8 +27,8 @@ const ORPHANS: &str = " WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles r WHERE r.oid = v.created_by::oid) ORDER BY 1, 2, 3"; -/// `warren.orphans()`: what names a dropped role (`kind` `graph`, `reader` -/// or `vector`), with the graph and the dropped role's oid. A superuser +/// `warren.orphans()`: what names a dropped role (`kind` `graph`, +/// `quota`, `reader` or `vector`), with the graph and the dropped role's oid. A superuser /// and members of the extension's owner see every row; others see those /// of graphs they own. `PARALLEL SAFE`: it reads no graph. #[pg_extern(schema = "warren", volatile, parallel_safe)] @@ -60,8 +63,8 @@ fn visible_orphans() -> Result, CatalogError> } /// `warren.drop_orphans()`: drops every graph whose owner was dropped (as -/// `drop_graph` does), removes reader entries that name dropped roles and -/// deletes the vectors whose creator was dropped. For a superuser or a +/// `drop_graph` does), removes reader entries and quotas that name dropped +/// roles and deletes the vectors whose creator was dropped. For a superuser or a /// member of the extension's owner (DESIGN.md §14 item 45). Returns the /// number of graphs dropped. #[pg_extern(schema = "warren")] @@ -79,7 +82,7 @@ macro_rules! dropped { /// Each pass of the sweep: a query that locks the rows naming a dropped /// role and returns their keys, then the statement that changes those rows /// (`$1`, their keys), checking each again. -const PASSES: [(&str, &str); 3] = [ +const PASSES: [(&str, &str); 4] = [ ( concat!( "SELECT graph_id::int8 FROM warren.graphs g WHERE ", @@ -121,6 +124,17 @@ const PASSES: [(&str, &str); 3] = [ dropped!("v.created_by") ), ), + ( + concat!( + "SELECT graph_id::int8 FROM warren.vector_quotas q WHERE ", + dropped!("q.role"), + " FOR UPDATE" + ), + concat!( + "DELETE FROM warren.vector_quotas q WHERE graph_id = ANY ($1) AND ", + dropped!("q.role") + ), + ), ]; fn sweep() -> Result { @@ -130,8 +144,6 @@ fn sweep() -> Result { // row would re-check it (EvalPlanQual) under its first snapshot, which // misses a role created by the transaction it waited for, and so would // drop a graph adopted for that role, or prune a reader just granted. - // Quotas (work breakdown step 7) add a pass for the rows of - // `warren.vector_quotas` that name dropped roles. let dropped = as_catalog_owner(|| { Spi::connect_mut(|client| { let mut changed = Vec::with_capacity(PASSES.len()); @@ -158,7 +170,7 @@ mod tests { use pgrx::prelude::*; use pgrx::spi::Error; - use crate::fixture::{failure_as, neighbouring_graphs, ownership, rows}; + use crate::fixture::{failure_as, neighbouring_graphs, ownership, quotas, rows}; /// [`neighbouring_graphs`], then a bare `DROP ROLE dr_owner`; returns /// its OID. @@ -173,11 +185,12 @@ mod tests { const LISTED: &str = "SELECT kind || ':' || graph || ':' || role::text FROM warren.orphans()"; #[pg_test] - fn orphans_lists_graphs_reader_entries_and_vectors_of_dropped_roles() -> Result<(), Error> { + fn orphans_lists_graphs_quotas_reader_entries_and_vectors_of_dropped_roles() -> Result<(), Error> + { let gone = orphaned()?; assert_eq!( rows(LISTED)?, - Some(format!("graph:dr:{gone} reader:dk:{gone} vector:dk:{gone}")) + Some(format!("graph:dr:{gone} quota:dk:{gone} reader:dk:{gone} vector:dk:{gone}")) ); Ok(()) } @@ -193,7 +206,7 @@ mod tests { Spi::run("SET ROLE dk_stranger")?; let strangers = rows(LISTED)?; Spi::run("RESET ROLE")?; - assert_eq!(owners, Some(format!("reader:dk:{gone} vector:dk:{gone}"))); + assert_eq!(owners, Some(format!("quota:dk:{gone} reader:dk:{gone} vector:dk:{gone}"))); assert_eq!(strangers.as_deref(), Some("")); Ok(()) } @@ -203,6 +216,7 @@ mod tests { orphaned()?; assert_eq!(Spi::get_one::("SELECT warren.drop_orphans()")?, Some(1)); assert_eq!(ownership()?.as_deref(), Some("dk:dk_owner:{} / dk.theirs:dk_owner")); + assert_eq!(quotas()?.as_deref(), Some("")); assert_eq!(rows(LISTED)?.as_deref(), Some("")); Ok(()) } @@ -226,7 +240,10 @@ mod tests { Spi::run("SET ROLE dr_member")?; let listed = rows(LISTED)?; Spi::run("RESET ROLE")?; - assert_eq!(listed, Some(format!("graph:dr:{gone} reader:dk:{gone} vector:dk:{gone}"))); + assert_eq!( + listed, + Some(format!("graph:dr:{gone} quota:dk:{gone} reader:dk:{gone} vector:dk:{gone}")) + ); assert_eq!(failure_as("dr_member", "warren.drop_orphans()")?, None); assert_eq!(ownership()?.as_deref(), Some("dk:dk_owner:{} / dk.theirs:dk_owner")); Ok(()) @@ -263,7 +280,10 @@ mod tests { let setter = rows(LISTED)?; Spi::run("RESET ROLE")?; assert_eq!(weak.as_deref(), Some("")); - assert_eq!(setter, Some(format!("graph:dr:{gone} reader:dk:{gone} vector:dk:{gone}"))); + assert_eq!( + setter, + Some(format!("graph:dr:{gone} quota:dk:{gone} reader:dk:{gone} vector:dk:{gone}")) + ); assert_eq!(failure_as("dr_setter", "warren.drop_orphans()")?, None); Ok(()) } diff --git a/crates/pg_warren/src/fixture.rs b/crates/pg_warren/src/fixture.rs index c291b2d3..7d236be5 100644 --- a/crates/pg_warren/src/fixture.rs +++ b/crates/pg_warren/src/fixture.rs @@ -309,9 +309,9 @@ pub(crate) fn statement_failure_as(role: &str, statement: &str) -> Result