use std::cell::RefCell;
use std::collections::HashMap;
use areev_core::error::{AreevError, Result};
use tokio_postgres::types::{IsNull, ToSql, Type};
use turso::Value;
use crate::db::{db_err, Db, Row, WriteIds};
use crate::{pb, pi, pt};
pub(crate) fn pg_err(e: tokio_postgres::Error) -> AreevError {
match e.as_db_error() {
Some(db) => AreevError::Storage(format!(
"postgres error {}: {}",
db.code().code(),
db.message()
)),
None => {
let mut msg = e.to_string();
let mut source = std::error::Error::source(&e);
for _ in 0..8 {
let Some(cause) = source else { break };
msg.push_str(": ");
msg.push_str(&cause.to_string());
source = cause.source();
}
AreevError::Storage(msg)
}
}
}
pub(crate) const PG_SCHEMA: &[&str] = &[
"CREATE TABLE IF NOT EXISTS meta(k text PRIMARY KEY, v text)",
"CREATE TABLE IF NOT EXISTS terms(id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, term text NOT NULL, term_hash bytea NOT NULL)",
"CREATE TABLE IF NOT EXISTS counters(name text PRIMARY KEY, v bigint NOT NULL)",
"CREATE TABLE IF NOT EXISTS grains(
seq bigint PRIMARY KEY,
hash bytea NOT NULL,
ns bigint, gtype bigint, created_at bigint,
s bigint, p bigint, o bigint,
vf bigint, vt bigint,
svf bigint, svt bigint,
superseded_by bytea, supersedes bytea,
text text,
blob bytea NOT NULL)",
"CREATE UNIQUE INDEX IF NOT EXISTS idx_grains_hash ON grains(hash)",
"CREATE INDEX IF NOT EXISTS idx_grains_ns_s ON grains(ns, s, p)",
"CREATE TABLE IF NOT EXISTS embeddings(seq bigint PRIMARY KEY)",
"CREATE TABLE IF NOT EXISTS fts_vocab(id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, term text UNIQUE)",
"CREATE TABLE IF NOT EXISTS fts_post(term bigint, seq bigint, ns bigint, tf bigint)",
"CREATE INDEX IF NOT EXISTS idx_fts_post ON fts_post(term, ns)",
"CREATE INDEX IF NOT EXISTS idx_fts_post_seq ON fts_post(seq)",
"CREATE TABLE IF NOT EXISTS fts_doc(seq bigint PRIMARY KEY, len bigint)",
"CREATE TABLE IF NOT EXISTS triples(ns bigint, s bigint, p bigint, o bigint, seq bigint, cur bigint)",
"CREATE INDEX IF NOT EXISTS idx_spo ON triples(ns,s,p,o,seq)",
"CREATE INDEX IF NOT EXISTS idx_pos ON triples(ns,p,o,s,seq)",
"CREATE INDEX IF NOT EXISTS idx_triples_seq ON triples(seq)",
"CREATE TABLE IF NOT EXISTS osp(ns bigint, o bigint, s bigint, p bigint, seq bigint, cur bigint)",
"CREATE INDEX IF NOT EXISTS idx_osp ON osp(ns,o,s)",
"CREATE INDEX IF NOT EXISTS idx_osp_seq ON osp(seq)",
"CREATE TABLE IF NOT EXISTS entity_latest(ns bigint, s bigint, p bigint, o bigint, seq bigint, hash bytea, PRIMARY KEY(ns,s,p))",
"CREATE TABLE IF NOT EXISTS heads(ns bigint, s bigint, p bigint, seq bigint, hash bytea, created_at bigint, PRIMARY KEY(ns,s,p,seq))",
"CREATE TABLE IF NOT EXISTS oplog(op_seq bigint PRIMARY KEY, hlc bigint, op bigint, hash bytea, ns bigint)",
"ALTER TABLE oplog ADD COLUMN IF NOT EXISTS ns bigint",
"UPDATE oplog o SET ns = g.ns FROM grains g WHERE g.hash = o.hash AND o.ns IS NULL",
"CREATE INDEX IF NOT EXISTS idx_oplog_ns ON oplog(ns, op_seq)",
"CREATE TABLE IF NOT EXISTS thread_idx(ns bigint, session bigint, seq bigint)",
"CREATE INDEX IF NOT EXISTS idx_thread ON thread_idx(ns, session, seq)",
"CREATE TABLE IF NOT EXISTS prov_idx(ns bigint, parent bytea, seq bigint)",
"CREATE INDEX IF NOT EXISTS idx_prov ON prov_idx(ns, parent, seq)",
"CREATE TABLE IF NOT EXISTS run_idx(ns bigint, run bigint, seq bigint)",
"CREATE INDEX IF NOT EXISTS idx_run ON run_idx(ns, run, seq)",
"CREATE TABLE IF NOT EXISTS corpus_idx(seq bigint PRIMARY KEY)",
"CREATE TABLE IF NOT EXISTS ns_reg(ns bigint PRIMARY KEY, n bigint NOT NULL)",
"CREATE TABLE IF NOT EXISTS blobs(hash bytea PRIMARY KEY, body bytea NOT NULL)",
];
pub(crate) const PG_SEED: &[&str] = &[
"INSERT INTO counters(name, v) SELECT 'seq', COALESCE(MAX(seq),0) FROM grains \
ON CONFLICT (name) DO UPDATE SET v = GREATEST(counters.v, EXCLUDED.v)",
"INSERT INTO counters(name, v) SELECT 'op', COALESCE(MAX(op_seq),0) FROM oplog \
ON CONFLICT (name) DO UPDATE SET v = GREATEST(counters.v, EXCLUDED.v)",
"INSERT INTO counters(name, v) SELECT 'hlc', COALESCE(MAX(hlc),0) FROM oplog \
ON CONFLICT (name) DO UPDATE SET v = GREATEST(counters.v, EXCLUDED.v)",
"DO $$ BEGIN \
IF NOT EXISTS (SELECT 1 FROM pg_attribute a \
JOIN pg_class c ON a.attrelid = c.oid \
JOIN pg_namespace n ON c.relnamespace = n.oid \
WHERE n.nspname = current_schema() AND c.relname = 'terms' \
AND a.attname = 'id' AND a.attidentity <> '') THEN \
EXECUTE 'ALTER TABLE terms ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY'; \
PERFORM setval(pg_get_serial_sequence('terms','id'), \
GREATEST((SELECT COALESCE(MAX(id),0) FROM terms), 1)); \
END IF; \
END $$",
"DO $$ DECLARE r record; BEGIN \
IF NOT EXISTS (SELECT 1 FROM information_schema.columns \
WHERE table_schema = current_schema() AND table_name = 'terms' \
AND column_name = 'term_hash') THEN \
EXECUTE 'ALTER TABLE terms ADD COLUMN term_hash bytea'; \
EXECUTE 'UPDATE terms SET term_hash = sha256(convert_to(term, ''UTF8'')) WHERE term_hash IS NULL'; \
EXECUTE 'ALTER TABLE terms ALTER COLUMN term_hash SET NOT NULL'; \
END IF; \
IF NOT EXISTS (SELECT 1 FROM pg_indexes \
WHERE schemaname = current_schema() AND tablename = 'terms' \
AND indexname = 'idx_terms_hash') THEN \
EXECUTE 'CREATE UNIQUE INDEX idx_terms_hash ON terms(term_hash)'; \
END IF; \
FOR r IN SELECT c.conname FROM pg_constraint c \
JOIN pg_class t ON c.conrelid = t.oid \
JOIN pg_namespace n ON t.relnamespace = n.oid \
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attname = 'term' \
WHERE n.nspname = current_schema() AND t.relname = 'terms' \
AND c.contype = 'u' AND c.conkey = ARRAY[a.attnum] LOOP \
EXECUTE format('ALTER TABLE terms DROP CONSTRAINT %I', r.conname); \
END LOOP; \
FOR r IN SELECT ic.relname AS idxname FROM pg_index i \
JOIN pg_class t ON i.indrelid = t.oid \
JOIN pg_class ic ON i.indexrelid = ic.oid \
JOIN pg_namespace n ON t.relnamespace = n.oid \
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attname = 'term' \
WHERE n.nspname = current_schema() AND t.relname = 'terms' \
AND i.indisunique AND i.indnkeyatts = 1 AND i.indkey[0] = a.attnum \
AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conindid = i.indexrelid) LOOP \
EXECUTE format('DROP INDEX %I', r.idxname); \
END LOOP; \
END $$",
];
pub(crate) const PG_SCHEMA_VERSION: &str = "2";
pub(crate) const PG_ROLLING_SAFE_FROM: &str = "1";
#[derive(Clone, Copy)]
pub(crate) struct PgStamp {
pub(crate) table: &'static str,
pub(crate) key: &'static str,
pub(crate) version: &'static str,
}
pub(crate) const STORE_STAMP: PgStamp = PgStamp {
table: "meta",
key: "pg_schema",
version: PG_SCHEMA_VERSION,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ProvisionMode {
#[default]
Auto,
Never,
}
impl ProvisionMode {
fn parse(v: &str) -> Result<Self> {
match v {
"auto" => Ok(Self::Auto),
"never" => Ok(Self::Never),
other => Err(AreevError::Validation(format!(
"postgres URL: provision={other:?} is not a mode (auto|never)"
))),
}
}
}
pub fn provision_mode(url: &str) -> Result<ProvisionMode> {
let Some((_, query)) = url.split_once('?') else {
return Ok(ProvisionMode::Auto);
};
let mut mode = ProvisionMode::Auto;
for pair in query.split('&') {
if let Some(("provision", v)) = pair.split_once('=') {
mode = ProvisionMode::parse(v)?;
}
}
Ok(mode)
}
fn strip_param(url: &str, name: &str) -> String {
let Some((base, query)) = url.split_once('?') else {
return url.to_string();
};
let rest: Vec<&str> = query
.split('&')
.filter(|p| !matches!(p.split_once('='), Some((k, _)) if k == name))
.collect();
if rest.is_empty() {
base.to_string()
} else {
format!("{base}?{}", rest.join("&"))
}
}
pub fn strip_provision(url: &str) -> String {
strip_param(url, "provision")
}
pub fn strip_schema(url: &str) -> String {
strip_param(url, "schema")
}
pub const META_SCHEMA_PARAM: &str = "meta_schema";
pub fn meta_schema(url: &str) -> Result<Option<String>> {
let Some((_, query)) = url.split_once('?') else {
return Ok(None);
};
let mut found = None;
for pair in query.split('&') {
if let Some((k, v)) = pair.split_once('=') {
if k == META_SCHEMA_PARAM {
if v.is_empty() {
return Err(AreevError::Validation(format!(
"postgres URL: {META_SCHEMA_PARAM}= names no schema — omit the \
parameter for the single-schema layout"
)));
}
found = Some(v.to_string());
}
}
}
Ok(found)
}
pub fn strip_meta_schema(url: &str) -> String {
strip_param(url, META_SCHEMA_PARAM)
}
pub const META_TABLES: &[&str] = &[
"counters",
"meta",
"ns_reg",
"telem_budget_stat",
"telem_grain_access",
"telem_meta",
"telem_query_stat",
"telem_recall_log",
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PgLayout {
schema: String,
meta_schema: String,
}
impl PgLayout {
pub fn single(schema: &str) -> Result<Self> {
Self::validate_name(schema)?;
Ok(Self { schema: schema.to_string(), meta_schema: schema.to_string() })
}
pub fn paired(schema: &str, meta_schema: &str) -> Result<Self> {
Self::validate_name(schema)?;
Self::validate_name(meta_schema)?;
if schema == meta_schema {
return Err(AreevError::Validation(format!(
"postgres URL: {META_SCHEMA_PARAM}={meta_schema:?} names the memory schema \
itself — the metadata schema must be a different schema; omit the parameter \
for the single-schema layout"
)));
}
Ok(Self { schema: schema.to_string(), meta_schema: meta_schema.to_string() })
}
pub fn from_url(url: &str, schema: &str) -> Result<Self> {
match meta_schema(url)? {
Some(m) => Self::paired(schema, &m),
None => Self::single(schema),
}
}
pub fn schema(&self) -> &str {
&self.schema
}
pub fn meta_schema(&self) -> Option<&str> {
self.is_paired().then_some(self.meta_schema.as_str())
}
pub fn is_paired(&self) -> bool {
self.schema != self.meta_schema
}
pub(crate) fn schema_for(&self, table: &str) -> &str {
if META_TABLES.contains(&table) {
&self.meta_schema
} else {
&self.schema
}
}
pub(crate) fn qualified(&self, table: &str) -> String {
format!("\"{}\".{table}", self.schema_for(table))
}
pub(crate) fn schemas(&self) -> Vec<&str> {
match self.meta_schema() {
Some(m) => vec![&self.schema, m],
None => vec![&self.schema],
}
}
fn describe(&self) -> String {
match self.meta_schema() {
Some(m) => format!("{:?} (metadata schema {m:?})", self.schema),
None => format!("{:?}", self.schema),
}
}
fn validate_name(schema: &str) -> Result<()> {
if schema.is_empty()
|| schema.len() > 63
|| !schema.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
|| schema.as_bytes()[0].is_ascii_digit()
{
return Err(AreevError::Validation(format!(
"postgres schema name must be [a-z_][a-z0-9_]* and <= 63 bytes, got {schema:?}"
)));
}
Ok(())
}
}
pub const DEFAULT_POOL_SIZE: usize = 8;
pub const POOL_ENV: &str = "AREEV_PG_POOL";
pub fn pool_size(url: &str) -> Result<usize> {
if let Some((_, query)) = url.split_once('?') {
let mut from_dsn = None;
for pair in query.split('&') {
if let Some(("pool", v)) = pair.split_once('=') {
from_dsn = Some(parse_pool_size("postgres URL: pool", v)?);
}
}
if let Some(n) = from_dsn {
return Ok(n);
}
}
match std::env::var(POOL_ENV) {
Ok(v) if !v.trim().is_empty() => parse_pool_size(&format!("${POOL_ENV}"), v.trim()),
_ => Ok(DEFAULT_POOL_SIZE),
}
}
fn parse_pool_size(what: &str, v: &str) -> Result<usize> {
match v.parse::<usize>() {
Ok(n) if n >= 1 => Ok(n),
_ => Err(AreevError::Validation(format!(
"{what}={v:?} must be a whole number of connections, at least 1"
))),
}
}
pub fn strip_pool(url: &str) -> String {
strip_param(url, "pool")
}
pub const DEFAULT_POOL_IDLE_SECS: u64 = 300;
pub const POOL_IDLE_ENV: &str = "AREEV_PG_POOL_IDLE_SECS";
const POOL_IDLE_PARAM: &str = "pool_idle_secs";
pub fn pool_idle_ttl(url: &str) -> Result<Option<std::time::Duration>> {
if let Some((_, query)) = url.split_once('?') {
let mut from_dsn = None;
for pair in query.split('&') {
if let Some((k, v)) = pair.split_once('=') {
if k == POOL_IDLE_PARAM {
from_dsn = Some(parse_idle_secs(&format!("postgres URL: {POOL_IDLE_PARAM}"), v)?);
}
}
}
if let Some(t) = from_dsn {
return Ok(t);
}
}
match std::env::var(POOL_IDLE_ENV) {
Ok(v) if !v.trim().is_empty() => parse_idle_secs(&format!("${POOL_IDLE_ENV}"), v.trim()),
_ => Ok(Some(std::time::Duration::from_secs(DEFAULT_POOL_IDLE_SECS))),
}
}
fn parse_idle_secs(what: &str, v: &str) -> Result<Option<std::time::Duration>> {
match v.parse::<u64>() {
Ok(0) => Ok(None),
Ok(n) => Ok(Some(std::time::Duration::from_secs(n))),
Err(_) => Err(AreevError::Validation(format!(
"{what}={v:?} must be a whole number of seconds an idle connection is kept \
(0 to keep them for the life of the process)"
))),
}
}
pub fn strip_pool_idle(url: &str) -> String {
strip_param(url, POOL_IDLE_PARAM)
}
fn describe_ttl(ttl: Option<std::time::Duration>) -> String {
match ttl {
Some(d) => format!("{}s", d.as_secs()),
None => "never".to_string(),
}
}
pub(crate) struct PooledConn {
client: tokio_postgres::Client,
cache: HashMap<String, tokio_postgres::Statement>,
ef_search: Option<usize>,
exact_scan: bool,
}
pub(crate) struct PgPool {
rt: tokio::runtime::Runtime,
url: String,
cap: usize,
idle_ttl: Option<std::time::Duration>,
sem: std::sync::Arc<tokio::sync::Semaphore>,
idle: std::sync::Mutex<Vec<(std::time::Instant, PooledConn)>>,
}
pub(crate) struct Checkout {
pool: std::sync::Arc<PgPool>,
conn: Option<PooledConn>,
_permit: tokio::sync::OwnedSemaphorePermit,
broken: bool,
}
impl Checkout {
pub(crate) fn conn(&mut self) -> &mut PooledConn {
self.conn.as_mut().expect("a checkout holds its connection until drop")
}
pub(crate) fn client(&self) -> &tokio_postgres::Client {
&self.conn.as_ref().expect("a checkout holds its connection until drop").client
}
pub(crate) fn note(&mut self, e: &AreevError) {
if self.client().is_closed() || PgDb::is_connection_dead(e) {
self.broken = true;
}
}
}
impl Drop for Checkout {
fn drop(&mut self) {
if let Some(c) = self.conn.take() {
if !self.broken && !c.client.is_closed() {
if let Ok(mut idle) = self.pool.idle.lock() {
idle.push((std::time::Instant::now(), c));
}
}
}
}
}
#[derive(Default)]
struct Registry {
pools: HashMap<String, std::sync::Arc<PgPool>>,
reaping: bool,
}
static POOLS: std::sync::OnceLock<std::sync::Mutex<Registry>> = std::sync::OnceLock::new();
fn registry() -> std::sync::MutexGuard<'static, Registry> {
POOLS.get_or_init(Default::default).lock().unwrap_or_else(|p| p.into_inner())
}
fn pool_key(url: &str) -> String {
["pool", POOL_IDLE_PARAM, "provision", "schema", META_SCHEMA_PARAM]
.iter()
.fold(url.to_string(), |acc, p| strip_param(&acc, p))
}
#[cfg(feature = "conformance")]
pub fn pool_is_registered(url: &str) -> bool {
registry().pools.contains_key(&pool_key(url))
}
const MIN_REAP_TICK: std::time::Duration = std::time::Duration::from_millis(250);
const MAX_REAP_TICK: std::time::Duration = std::time::Duration::from_secs(30);
fn start_reaper(reg: &mut Registry) {
if reg.reaping || !reg.pools.values().any(|p| p.idle_ttl.is_some()) {
return;
}
let spawned = std::thread::Builder::new().name("areev-pg-reap".into()).spawn(|| {
while let Some(tick) = sweep() {
std::thread::sleep(tick);
}
});
reg.reaping = spawned.is_ok();
}
fn sweep() -> Option<std::time::Duration> {
let mut evicted: Vec<std::sync::Arc<PgPool>> = Vec::new();
let tick = {
let mut reg = registry();
let now = std::time::Instant::now();
let mut keep = HashMap::with_capacity(reg.pools.len());
for (key, p) in std::mem::take(&mut reg.pools) {
let left = p.reap_idle(now);
if left == 0 && p.idle_ttl.is_some() && std::sync::Arc::strong_count(&p) == 1 {
evicted.push(p);
} else {
keep.insert(key, p);
}
}
reg.pools = keep;
match reg.pools.values().filter_map(|p| p.idle_ttl).min() {
Some(ttl) => Some((ttl / 2).clamp(MIN_REAP_TICK, MAX_REAP_TICK)),
None => {
reg.reaping = false;
None
}
}
};
drop(evicted);
tick
}
impl PgPool {
pub(crate) fn for_url(url: &str) -> Result<(std::sync::Arc<PgPool>, Option<String>)> {
let want = pool_size(url)?;
let want_ttl = pool_idle_ttl(url)?;
let key = pool_key(url);
let mut reg = registry();
if let Some(p) = reg.pools.get(&key) {
let mut warn: Vec<String> = Vec::new();
if p.cap != want {
warn.push(format!(
"postgres pool for this DSN was sized at {} connections by an earlier open \
in this process; ?pool={want} (or ${POOL_ENV}) does not resize it",
p.cap
));
}
if p.idle_ttl != want_ttl {
warn.push(format!(
"postgres pool for this DSN reaps idle connections after {} by an earlier \
open in this process; ?{POOL_IDLE_PARAM}={} (or ${POOL_IDLE_ENV}) does not \
change it",
describe_ttl(p.idle_ttl),
describe_ttl(want_ttl)
));
}
let p = std::sync::Arc::clone(p);
return Ok((p, (!warn.is_empty()).then(|| warn.join("; "))));
}
let workers = want.clamp(2, 8);
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(workers)
.thread_name("areev-pg")
.enable_all()
.build()
.map_err(db_err)?;
let p = std::sync::Arc::new(PgPool {
rt,
url: key.clone(),
cap: want,
idle_ttl: want_ttl,
sem: std::sync::Arc::new(tokio::sync::Semaphore::new(want)),
idle: std::sync::Mutex::new(Vec::new()),
});
reg.pools.insert(key, std::sync::Arc::clone(&p));
start_reaper(&mut reg);
Ok((p, None))
}
fn reap_idle(&self, now: std::time::Instant) -> usize {
let mut idle = self.idle.lock().unwrap_or_else(|p| p.into_inner());
let Some(ttl) = self.idle_ttl else {
return idle.len();
};
let mut kept = Vec::with_capacity(idle.len());
let mut doomed = Vec::new();
for (since, c) in idle.drain(..) {
if now.duration_since(since) < ttl && !c.client.is_closed() {
kept.push((since, c));
} else {
doomed.push(c);
}
}
*idle = kept;
let left = idle.len();
drop(idle);
drop(doomed);
left
}
pub(crate) fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
self.rt.block_on(f)
}
pub(crate) fn checkout(self: &std::sync::Arc<Self>) -> Result<Checkout> {
let permit = self
.rt
.block_on(std::sync::Arc::clone(&self.sem).acquire_owned())
.map_err(db_err)?;
let idle = loop {
let next = self.idle.lock().unwrap_or_else(|p| p.into_inner()).pop();
match next {
Some((_, c)) if c.client.is_closed() => continue,
other => break other.map(|(_, c)| c),
}
};
let conn = match idle {
Some(c) => c,
None => self.dial()?,
};
Ok(Checkout { pool: std::sync::Arc::clone(self), conn: Some(conn), _permit: permit, broken: false })
}
fn dial(&self) -> Result<PooledConn> {
let client = crate::pgtls::connect(&self.rt, &self.url)?;
self.rt
.block_on(client.batch_execute("SET search_path TO public, ext"))
.map_err(pg_err)?;
Ok(PooledConn { client, cache: HashMap::new(), ef_search: None, exact_scan: false })
}
}
pub(crate) struct PgDb {
pool: std::sync::Arc<PgPool>,
layout: PgLayout,
pinned: RefCell<Option<Checkout>>,
in_txn: std::cell::Cell<bool>,
ann_ef_search: std::cell::Cell<Option<usize>>,
exact_scan: std::cell::Cell<bool>,
xlate: RefCell<HashMap<String, String>>,
stats: RefCell<Option<(i64, i64)>>,
bootstrap_skipped: bool,
pool_warning: Option<String>,
}
fn txn_prelude(want_ef: Option<usize>, want_exact: bool, conn: &PooledConn) -> String {
let mut sql = String::from("BEGIN");
match (want_ef, conn.ef_search) {
(Some(ef), have) if have != Some(ef) => {
sql.push_str(&format!("; SET LOCAL hnsw.ef_search = {ef}"));
}
(None, Some(_)) => sql.push_str("; SET LOCAL hnsw.ef_search TO DEFAULT"),
_ => {}
}
if want_exact && !conn.exact_scan {
sql.push_str("; SET LOCAL enable_indexscan = off; SET LOCAL enable_bitmapscan = off");
} else if !want_exact && conn.exact_scan {
sql.push_str("; SET LOCAL enable_indexscan TO DEFAULT; SET LOCAL enable_bitmapscan TO DEFAULT");
}
sql
}
impl PgDb {
pub(crate) fn open(
url: &str,
schema: &str,
bootstrap: &[&str],
read_only: bool,
stamp: Option<PgStamp>,
) -> Result<Self> {
let layout = PgLayout::from_url(url, schema)?;
let (pool, pool_warning) = PgPool::for_url(url)?;
let mut co = pool.checkout()?;
let opened = Self::open_on(&pool, co.client(), url, &layout, bootstrap, read_only, stamp);
let bootstrap_skipped = match opened {
Ok(skipped) => skipped,
Err(e) => {
co.note(&e);
return Err(e);
}
};
drop(co);
Ok(Self {
pool,
layout,
pinned: RefCell::new(None),
in_txn: std::cell::Cell::new(false),
ann_ef_search: std::cell::Cell::new(None),
exact_scan: std::cell::Cell::new(false),
xlate: RefCell::new(HashMap::new()),
stats: RefCell::new(None),
bootstrap_skipped,
pool_warning,
})
}
fn open_on(
pool: &PgPool,
client: &tokio_postgres::Client,
url: &str,
layout: &PgLayout,
bootstrap: &[&str],
read_only: bool,
stamp: Option<PgStamp>,
) -> Result<bool> {
if read_only {
pool.block_on(Self::verify_read_only(client, layout))?;
return Ok(false);
}
let mut skipped = false;
if let Some(st) = stamp {
skipped = pool.block_on(Self::schema_stamp(client, layout, st))?.as_deref()
== Some(st.version);
}
if skipped {
return Ok(true);
}
pool.block_on(Self::check_layout(client, layout))?;
if provision_mode(url)? == ProvisionMode::Never {
return Err(Self::not_provisioned(client, pool, layout, stamp));
}
match Self::bootstrap_once(pool, client, layout, bootstrap, stamp) {
Ok(()) => Ok(false),
Err(e) if Self::is_lost_bootstrap_race(&e) => {
let current = match stamp {
Some(st) => {
pool.block_on(Self::schema_stamp(client, layout, st))?.as_deref()
== Some(st.version)
}
None => false,
};
if current {
Ok(true)
} else {
Self::bootstrap_once(pool, client, layout, bootstrap, stamp).map(|()| false)
}
}
Err(e) => Err(e),
}
}
fn is_lost_bootstrap_race(e: &AreevError) -> bool {
let AreevError::Storage(msg) = e else { return false };
msg.contains("postgres error 23505")
|| msg.contains("postgres error 42P06")
|| msg.contains("postgres error 42710")
}
fn bootstrap_once(
pool: &PgPool,
client: &tokio_postgres::Client,
layout: &PgLayout,
bootstrap: &[&str],
stamp: Option<PgStamp>,
) -> Result<()> {
let schema = layout.schema();
pool.block_on(async {
client.batch_execute("BEGIN").await.map_err(pg_err)?;
let boot = async {
client
.query_one(
"SELECT pg_advisory_xact_lock(hashtext('areev_bootstrap'), hashtext($1))",
&[&schema],
)
.await
.map_err(pg_err)?;
for s in layout.schemas() {
client
.batch_execute(&format!("CREATE SCHEMA IF NOT EXISTS \"{s}\""))
.await
.map_err(pg_err)?;
}
client
.batch_execute(&format!(
"SET LOCAL search_path TO \"{schema}\", public, ext"
))
.await
.map_err(pg_err)?;
for sql in bootstrap {
let routed = qualify_tables(sql, layout);
client.batch_execute(&routed).await.map_err(|e| {
AreevError::Storage(format!(
"bootstrap failed: {} — in: {routed}",
pg_err(e)
))
})?;
}
if let Some(st) = stamp {
client
.execute(
&format!(
"INSERT INTO {}(k, v) VALUES ($1, $2) \
ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v",
layout.qualified(st.table)
),
&[&st.key, &st.version],
)
.await
.map_err(pg_err)?;
}
Ok::<_, AreevError>(())
}
.await;
match boot {
Ok(()) => client.batch_execute("COMMIT").await.map_err(pg_err),
Err(e) => {
let _ = client.batch_execute("ROLLBACK").await;
Err(e)
}
}
})
}
pub(crate) fn bootstrap_skipped(&self) -> bool {
self.bootstrap_skipped
}
pub(crate) fn pool_warning(&self) -> Option<&str> {
self.pool_warning.as_deref()
}
async fn schema_stamp(
client: &tokio_postgres::Client,
layout: &PgLayout,
stamp: PgStamp,
) -> Result<Option<String>> {
let table = layout.qualified(stamp.table);
if !Self::relation_present(client, &table).await? {
return Ok(None);
}
let row = client
.query_opt(&format!("SELECT v FROM {table} WHERE k = $1"), &[&stamp.key])
.await
.map_err(pg_err)?;
Ok(row.and_then(|r| r.get::<_, Option<String>>(0)))
}
async fn relation_present(client: &tokio_postgres::Client, qualified: &str) -> Result<bool> {
Ok(client
.query_one("SELECT to_regclass($1) IS NOT NULL", &[&qualified])
.await
.map_err(pg_err)?
.get::<_, bool>(0))
}
async fn schemas_exist(client: &tokio_postgres::Client, layout: &PgLayout) -> Result<bool> {
for s in layout.schemas() {
let exists = client
.query_one(
"SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)",
&[&s],
)
.await
.map_err(pg_err)?
.get::<_, bool>(0);
if !exists {
return Ok(false);
}
}
Ok(true)
}
async fn check_layout(client: &tokio_postgres::Client, layout: &PgLayout) -> Result<()> {
let schema = layout.schema();
let in_schema_meta = Self::relation_present(client, &format!("\"{schema}\".meta")).await?;
match layout.meta_schema() {
Some(m) if in_schema_meta => Err(AreevError::LayoutMismatch(format!(
"postgres schema {schema:?} carries its engine metadata in-schema (the \
single-schema layout), but this DSN names {META_SCHEMA_PARAM}={m:?} — \
opening it paired would split counters, the namespace registry, legal holds, \
retention policies and saved queries across two schemas. Either drop \
{META_SCHEMA_PARAM} from the DSN, or move the metadata tables explicitly \
(`ALTER TABLE … SET SCHEMA`, see docs/deployment-profile.md \"Paired \
layout\") and retry"
))),
None if !in_schema_meta
&& Self::relation_present(client, &format!("\"{schema}\".grains")).await? =>
{
Err(AreevError::LayoutMismatch(format!(
"postgres schema {schema:?} holds memory tables but no meta table — the \
shape of a paired-layout memory whose engine metadata lives in a separate \
schema — and this DSN names no {META_SCHEMA_PARAM}. Add \
?{META_SCHEMA_PARAM}=<its metadata schema> to the DSN; a single-schema \
open would bootstrap a second, empty set of engine metadata beside the \
real one"
)))
}
_ => Ok(()),
}
}
fn not_provisioned(
client: &tokio_postgres::Client,
pool: &PgPool,
layout: &PgLayout,
stamp: Option<PgStamp>,
) -> AreevError {
let version = stamp.map(|s| s.version).unwrap_or(PG_SCHEMA_VERSION);
let exists = pool.block_on(Self::schemas_exist(client, layout)).unwrap_or(false);
let what = layout.describe();
let schema = layout.schema();
let hint = match layout.meta_schema() {
Some(m) => format!("`areev provision --db <dsn> --schema {schema} --meta-schema {m}`"),
None => format!("`areev provision --db <dsn> --schema {schema}`"),
};
AreevError::SchemaNotProvisioned(if exists {
format!(
"postgres schema {what} exists but is not stamped at schema version \
{version}, and this DSN says provision=never — so no bootstrap DDL was \
attempted. It was created by an older build, or a migration did not finish: \
run {hint} (or your migration job) with a role that owns the schema, then retry"
)
} else {
format!(
"postgres schema {what} does not exist, and this DSN says provision=never \
— so no CREATE SCHEMA was attempted. Either a schema name is wrong, or the \
memory has never been created: run {hint} with a role that may create it, \
then retry"
)
})
}
async fn verify_read_only(
client: &tokio_postgres::Client,
layout: &PgLayout,
) -> Result<()> {
let schema = layout.schema();
let what = layout.describe();
Self::check_layout(client, layout).await?;
if !Self::schemas_exist(client, layout).await? {
return Err(AreevError::ReadOnlyOpenFailed(format!(
"postgres schema {what} does not exist — a read-only open never runs \
bootstrap DDL (CREATE SCHEMA included), so it cannot create it. Either a \
schema name is wrong, or the memory has never been created: have the owning \
role open it read-write once (or run the migration), then retry read-only"
)));
}
const CORE_TABLES: &[&str] = &["meta", "terms", "grains", "oplog", "fts_doc"];
let mut missing: Vec<String> = Vec::new();
for table in CORE_TABLES {
let qualified = layout.qualified(table);
if !Self::relation_present(client, &qualified).await? {
missing.push(qualified);
}
}
if !missing.is_empty() {
return Err(AreevError::ReadOnlyOpenFailed(format!(
"postgres schema {what} exists but is not fully initialized — missing \
table(s): {} — a read-only open never runs bootstrap DDL to add them. Have the \
owning role open this memory read-write once to finish migrating it, then \
retry read-only",
missing.join(", ")
)));
}
let keyed = client
.query_one(
"SELECT EXISTS(SELECT 1 FROM information_schema.columns \
WHERE table_schema = $1 AND table_name = 'terms' AND column_name = 'term_hash')",
&[&schema],
)
.await
.map_err(pg_err)?
.get::<_, bool>(0);
if !keyed {
return Err(AreevError::ReadOnlyOpenFailed(format!(
"postgres schema {schema:?} predates the digest-keyed dictionary (terms.term_hash \
is missing) — a read-only open never runs the migration that adds it. Have the \
owning role open this memory read-write once, then retry read-only"
)));
}
Ok(())
}
fn is_connection_dead(e: &AreevError) -> bool {
let AreevError::Storage(msg) = e else {
return false;
};
msg.contains("57P01")
|| msg.contains("57P02")
|| msg.contains("57P03")
|| msg.contains("postgres error 08")
|| msg.contains("connection closed")
|| msg.contains("connection unexpectedly closed")
}
fn with_conn<T>(&self, f: impl FnOnce(&PgPool, &mut PooledConn) -> Result<T>) -> Result<T> {
let mut pinned = self.pinned.borrow_mut();
if let Some(co) = pinned.as_mut() {
let r = f(&self.pool, co.conn());
if let Err(e) = &r {
co.note(e);
}
return r;
}
drop(pinned);
let mut co = self.pool.checkout()?;
let r = f(&self.pool, co.conn());
if let Err(e) = &r {
co.note(e);
}
r
}
fn align_session(&self, pool: &PgPool, conn: &mut PooledConn) -> Result<()> {
if self.in_txn.get() {
return Ok(());
}
let want_ef = self.ann_ef_search.get();
let want_exact = self.exact_scan.get();
let mut sql = String::new();
if conn.ef_search != want_ef {
match want_ef {
Some(ef) => sql.push_str(&format!("SET hnsw.ef_search = {ef}; ")),
None => sql.push_str("RESET hnsw.ef_search; "),
}
}
if conn.exact_scan != want_exact {
sql.push_str(if want_exact {
"SET enable_indexscan = off; SET enable_bitmapscan = off; "
} else {
"RESET enable_indexscan; RESET enable_bitmapscan; "
});
}
if sql.is_empty() {
return Ok(());
}
pool.block_on(conn.client.batch_execute(&sql)).map_err(pg_err)?;
conn.ef_search = want_ef;
conn.exact_scan = want_exact;
Ok(())
}
fn translated(&self, sql: &str, hot: bool) -> Result<String> {
if hot {
if let Some(t) = self.xlate.borrow().get(sql) {
return Ok(t.clone());
}
}
let t = qualify_tables(&translate(sql)?, &self.layout);
if hot {
self.xlate.borrow_mut().insert(sql.to_string(), t.clone());
}
Ok(t)
}
fn prepared(
pool: &PgPool,
conn: &mut PooledConn,
translated: &str,
) -> Result<tokio_postgres::Statement> {
if let Some(st) = conn.cache.get(translated) {
return Ok(st.clone());
}
let st = pool.block_on(conn.client.prepare(translated)).map_err(|e| {
AreevError::Storage(format!(
"prepare failed: {} — translated SQL: {translated}",
pg_err(e)
))
})?;
if conn.cache.len() >= 512 {
conn.cache.clear();
}
conn.cache.insert(translated.to_string(), st.clone());
Ok(st)
}
fn maybe_discard_session(&self, pool: &PgPool, conn: &mut PooledConn) -> Result<()> {
if self.in_txn.get() {
return Ok(());
}
let sql = match session_chaos_mode().as_deref() {
None => return Ok(()),
Some("deallocate") => "RESET ALL; DEALLOCATE ALL",
Some(_) => "RESET ALL",
};
pool.block_on(conn.client.batch_execute(sql)).map_err(pg_err)?;
conn.ef_search = None;
conn.exact_scan = false;
if sql.contains("DEALLOCATE") {
conn.cache.clear();
}
Ok(())
}
fn query_once(&self, sql: &str, params: Vec<Value>, hot: bool) -> Result<Vec<Row>> {
let translated = self.translated(sql, hot)?;
self.with_conn(|pool, conn| {
self.maybe_discard_session(pool, conn)?;
self.align_session(pool, conn)?;
let vals: Vec<PgVal> = params.into_iter().map(PgVal).collect();
let refs: Vec<&(dyn ToSql + Sync)> =
vals.iter().map(|v| v as &(dyn ToSql + Sync)).collect();
let rows = if hot {
let st = Self::prepared(pool, conn, &translated)?;
pool.block_on(conn.client.query(&st, &refs))
} else {
pool.block_on(conn.client.query(translated.as_str(), &refs))
}
.map_err(pg_err)
.map_err(|e| Self::forget_stale(conn, e))?;
rows.iter().map(row_to_row).collect()
})
}
fn execute_once(&self, sql: &str, params: Vec<Value>, hot: bool) -> Result<u64> {
let translated = self.translated(sql, hot)?;
self.with_conn(|pool, conn| {
self.maybe_discard_session(pool, conn)?;
self.align_session(pool, conn)?;
let vals: Vec<PgVal> = params.into_iter().map(PgVal).collect();
let refs: Vec<&(dyn ToSql + Sync)> =
vals.iter().map(|v| v as &(dyn ToSql + Sync)).collect();
if hot {
let st = Self::prepared(pool, conn, &translated)?;
pool.block_on(conn.client.execute(&st, &refs))
} else {
pool.block_on(conn.client.execute(translated.as_str(), &refs))
}
.map_err(pg_err)
.map_err(|e| Self::forget_stale(conn, e))
})
}
fn forget_stale(conn: &mut PooledConn, e: AreevError) -> AreevError {
if matches!(&e, AreevError::Storage(m) if m.starts_with("postgres error 26000")) {
conn.cache.clear();
}
e
}
fn run_query(&self, sql: &str, params: Vec<Value>, hot: bool) -> Result<Vec<Row>> {
match self.query_once(sql, params.clone(), hot) {
Ok(rows) => Ok(rows),
Err(e) => {
if self.stale_statement(&e, hot) || self.recover(&e, true) {
self.query_once(sql, params, hot)
} else {
Err(Self::explain_stale(e))
}
}
}
}
fn stale_statement(&self, e: &AreevError, hot: bool) -> bool {
if !hot {
return false;
}
let AreevError::Storage(msg) = e else { return false };
msg.starts_with("postgres error 26000") && !self.in_txn.get()
}
fn recover(&self, e: &AreevError, replayable: bool) -> bool {
if self.in_txn.get() {
return false;
}
Self::is_connection_dead(e) && replayable
}
fn explain_stale(e: AreevError) -> AreevError {
match e {
AreevError::Storage(msg) if msg.starts_with("postgres error 26000") => {
AreevError::Storage(format!(
"{msg} — a prepared statement was unknown to this backend \
mid-transaction, which is what a transaction-mode pooler that does \
not track prepared statements does; use PgBouncer 1.21+ with \
max_prepared_statements > 0 (or another pooler that tracks them), \
or session mode"
))
}
other => other,
}
}
fn run_execute(&self, sql: &str, params: Vec<Value>, hot: bool) -> Result<u64> {
match self.execute_once(sql, params.clone(), hot) {
Ok(n) => Ok(n),
Err(e) => {
if self.stale_statement(&e, hot) {
return self.execute_once(sql, params, hot);
}
Err(Self::explain_stale(e))
}
}
}
fn end_txn(&self, verb: &str) -> Result<()> {
let co = self.pinned.borrow_mut().take();
self.in_txn.set(false);
let Some(mut co) = co else {
return Ok(());
};
let r = self.pool.block_on(co.client().batch_execute(verb)).map_err(pg_err);
if let Err(e) = &r {
co.note(e);
}
r
}
}
impl Drop for PgDb {
fn drop(&mut self) {
if self.pinned.borrow().is_some() {
let _ = self.end_txn("ROLLBACK");
}
}
}
impl Db for PgDb {
fn execute(&self, sql: &str, params: Vec<Value>) -> Result<u64> {
self.run_execute(sql, params, false)
}
fn query(&self, sql: &str, params: Vec<Value>) -> Result<Vec<Row>> {
self.run_query(sql, params, false)
}
fn execute_hot(&self, sql: &'static str, params: Vec<Value>) -> Result<u64> {
self.run_execute(sql, params, true)
}
fn query_hot(&self, sql: &'static str, params: Vec<Value>) -> Result<Vec<Row>> {
self.run_query(sql, params, true)
}
fn begin(&self) -> Result<()> {
if self.pinned.borrow().is_some() {
return Ok(());
}
let mut co = self.pool.checkout()?;
let r = {
let conn = co.conn();
if session_chaos_mode().as_deref() == Some("deallocate-txn") {
self.pool
.block_on(conn.client.batch_execute("DEALLOCATE ALL"))
.map_err(pg_err)?;
conn.cache.clear();
}
let sql = txn_prelude(self.ann_ef_search.get(), self.exact_scan.get(), conn);
self.pool.block_on(conn.client.batch_execute(&sql)).map_err(pg_err)
};
match r {
Ok(()) => {
*self.pinned.borrow_mut() = Some(co);
self.in_txn.set(true);
Ok(())
}
Err(e) => {
co.note(&e);
Err(e)
}
}
}
fn commit(&self) -> Result<()> {
self.end_txn("COMMIT")
}
fn rollback(&self) -> Result<()> {
self.end_txn("ROLLBACK")
}
fn prefers_batched_reads(&self) -> bool {
true }
fn reserve_write(
&self,
n_seq: i64,
n_op: i64,
n_hlc: i64,
wall_hlc: i64,
hlc_floor: i64,
) -> Result<Option<WriteIds>> {
*self.stats.borrow_mut() = None;
let rows = self.run_query(
"UPDATE counters SET v = CASE name \
WHEN 'seq' THEN v + ?1 \
WHEN 'op' THEN v + ?2 \
ELSE GREATEST(v + ?3, ?4 + ?3 - 1, ?5) END \
WHERE name IN ('seq','op','hlc') RETURNING name, v",
vec![pi(n_seq), pi(n_op), pi(n_hlc), pi(wall_hlc), pi(hlc_floor)],
true,
)?;
let (mut seq_last, mut op_last, mut hlc_last) = (0i64, 0i64, 0i64);
for row in &rows {
match (row.text(0), row.i64(1)) {
(Some("seq"), Some(v)) => seq_last = v,
(Some("op"), Some(v)) => op_last = v,
(Some("hlc"), Some(v)) => hlc_last = v,
_ => {}
}
}
if rows.len() != 3 {
return Err(AreevError::Storage(format!(
"counters rows missing ({} of 3) — schema not seeded?",
rows.len()
)));
}
Ok(Some(WriteIds {
seq0: seq_last - n_seq + 1,
op0: op_last - n_op + 1,
hlc0: hlc_last - (n_hlc - 1).max(0),
}))
}
fn intern_term(&self, term: &str) -> Result<Option<i64>> {
let hash = term_hash(term);
let inserted = self.run_query(
"INSERT INTO terms(term, term_hash) VALUES (?1, ?2) ON CONFLICT (term_hash) DO NOTHING RETURNING id",
vec![pt(term), pb(hash.clone())],
true,
)?;
if let Some(id) = inserted.first().and_then(|r| r.i64(0)) {
return Ok(Some(id));
}
let existing =
self.run_query("SELECT id FROM terms WHERE term_hash = ?1", vec![pb(hash)], true)?;
match existing.first().and_then(|r| r.i64(0)) {
Some(id) => Ok(Some(id)),
None => Err(AreevError::Storage("terms insert vanished".into())),
}
}
fn lookup_term(&self, term: &str) -> Result<Option<i64>> {
Ok(self
.run_query(
"SELECT id FROM terms WHERE term_hash = ?1",
vec![pb(term_hash(term))],
true,
)?
.first()
.and_then(|r| r.i64(0)))
}
fn scrub_term(&self, id: i64, placeholder: &str) -> Result<u64> {
self.execute(
"UPDATE terms SET term = ?2, term_hash = ?4 WHERE id = ?1 \
AND NOT EXISTS (SELECT 1 FROM triples WHERE s=?1 OR p=?1 OR o=?1) \
AND NOT EXISTS (SELECT 1 FROM thread_idx WHERE session=?1) \
AND NOT EXISTS (SELECT 1 FROM run_idx WHERE run=?1) \
AND NOT EXISTS (SELECT 1 FROM grains WHERE ns=?1) \
AND term NOT LIKE ?3",
vec![pi(id), pt(placeholder), pt("\u{1}erased:%"), pb(term_hash(placeholder))],
)
}
fn lookup_term_str(&self, id: i64) -> Result<Option<String>> {
Ok(self
.run_query("SELECT term FROM terms WHERE id = ?1", vec![pi(id)], true)?
.first()
.and_then(|r| r.text(0).map(str::to_string)))
}
fn collection_stats(&self) -> Result<Option<(i64, i64)>> {
if let Some(s) = *self.stats.borrow() {
return Ok(Some(s));
}
let rows = self.run_query(
"SELECT COUNT(*), COALESCE(SUM(len),0)::bigint FROM fts_doc",
vec![],
true,
)?;
let s = rows.first().map(|r| (r.i64(0).unwrap_or(0), r.i64(1).unwrap_or(0)));
*self.stats.borrow_mut() = s;
Ok(s)
}
fn for_update(&self) -> &'static str {
" FOR UPDATE"
}
fn terms_with_prefix(&self, prefix: &str) -> Result<Option<Vec<(i64, String)>>> {
let mut pat = String::with_capacity(prefix.len() + 1);
for c in prefix.chars() {
if matches!(c, '%' | '_' | '\\') {
pat.push('\\');
}
pat.push(c);
}
pat.push('%');
let rows = self.run_query(
"SELECT id, term FROM terms WHERE term LIKE ?1 ESCAPE '\\'",
vec![pt(&pat)],
true,
)?;
Ok(Some(
rows.iter()
.filter_map(|r| Some((r.i64(0)?, r.text(1)?.to_string())))
.collect(),
))
}
fn seeds_state_at_open(&self) -> bool {
false
}
fn may_have_legacy_fts_index(&self) -> bool {
false
}
fn ensure_embeddings(&self, dim: usize) -> Result<()> {
let schema = self.layout.schema.clone();
self.with_conn(|pool, conn| {
pool.block_on(async {
let client = &conn.client;
let _ = client
.batch_execute("CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public")
.await;
client
.batch_execute(&format!(
"ALTER TABLE \"{schema}\".embeddings ADD COLUMN IF NOT EXISTS vec vector({dim})"
))
.await
.map_err(|e| {
AreevError::Storage(format!(
"cannot add vector({dim}) column (is the pgvector extension available?): {e}"
))
})?;
let row = client
.query_one(
"SELECT a.atttypmod FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname = $1 AND c.relname = 'embeddings'
AND a.attname = 'vec'",
&[&schema],
)
.await
.map_err(pg_err)?;
let existing = row.get::<_, i32>(0);
if existing > 0 && existing as usize != dim {
return Err(AreevError::Validation(format!(
"embeddings column holds vector({existing}) but the embedder produces dim {dim} \
— refusing to mix vector spaces (this is a hard error on the postgres backend)"
)));
}
Ok(())
})
})
}
fn ensure_ann_index(&self, m: usize, ef_construction: usize, ef_search: usize) -> Result<()> {
if !(2..=100).contains(&m) {
return Err(AreevError::Validation(format!(
"hnsw m must be 2..=100, got {m}"
)));
}
if !(4..=1000).contains(&ef_construction) {
return Err(AreevError::Validation(format!(
"hnsw ef_construction must be 4..=1000, got {ef_construction}"
)));
}
let schema = self.layout.schema.clone();
self.with_conn(|pool, conn| {
pool.block_on(async {
let client = &conn.client;
let has_vec = client
.query_one(
"SELECT count(*) FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname = $1 AND c.relname = 'embeddings'
AND a.attname = 'vec' AND NOT a.attisdropped",
&[&schema],
)
.await
.map_err(pg_err)?
.get::<_, i64>(0);
if has_vec == 0 {
return Err(AreevError::AnnIndexUnsupported(
"this memory has no embeddings yet — install an embedder and store at least \
one vector before building an index over them"
.into(),
));
}
client
.batch_execute(&format!(
"CREATE INDEX IF NOT EXISTS idx_embeddings_hnsw ON \"{schema}\".embeddings \
USING hnsw (vec vector_cosine_ops) WITH (m = {m}, ef_construction = {ef_construction})"
))
.await
.map_err(|e| {
AreevError::Storage(format!("cannot build the HNSW index over embeddings: {e}"))
})
})
})?;
self.ann_ef_search.set(Some(ef_search));
Ok(())
}
fn set_ann_ef_search(&self, ef_search: usize) -> Result<()> {
if !(1..=1000).contains(&ef_search) {
return Err(AreevError::Validation(format!(
"hnsw ef_search must be 1..=1000, got {ef_search}"
)));
}
self.ann_ef_search.set(Some(ef_search));
Ok(())
}
fn ann_ef_search(&self) -> Result<Option<usize>> {
if let Some(ef) = self.ann_ef_search.get() {
return Ok(Some(ef));
}
self.with_conn(|pool, conn| {
let row = pool
.block_on(conn.client.query_one("SELECT current_setting('hnsw.ef_search', true)", &[]))
.map_err(pg_err)?;
Ok(row.get::<_, Option<String>>(0).and_then(|v| v.parse().ok()))
})
}
fn drop_ann_index(&self) -> Result<()> {
let sql = format!("DROP INDEX IF EXISTS \"{}\".idx_embeddings_hnsw", self.layout.schema);
self.with_conn(|pool, conn| pool.block_on(conn.client.batch_execute(&sql)).map_err(pg_err))
}
fn set_exact_vector_scan(&self, on: bool) -> Result<()> {
self.exact_scan.set(on);
Ok(())
}
fn ann_index_name(&self) -> Result<Option<String>> {
let schema = self.layout.schema.clone();
self.with_conn(|pool, conn| {
let rows = pool
.block_on(conn.client.query(
"SELECT c.relname FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
JOIN pg_class t ON t.oid = i.indrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_am am ON am.oid = c.relam
WHERE n.nspname = $1 AND t.relname = 'embeddings'
AND am.amname = 'hnsw'",
&[&schema],
))
.map_err(pg_err)?;
Ok(rows.first().map(|r| r.get::<_, String>(0)))
})
}
}
#[derive(Debug)]
struct PgVal(Value);
impl ToSql for PgVal {
fn to_sql(
&self,
ty: &Type,
out: &mut tokio_postgres::types::private::BytesMut,
) -> std::result::Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
match &self.0 {
Value::Null => Ok(IsNull::Yes),
Value::Integer(i) => match *ty {
Type::INT8 => i.to_sql(ty, out),
Type::INT4 => (*i as i32).to_sql(&Type::INT4, out),
Type::FLOAT8 => (*i as f64).to_sql(&Type::FLOAT8, out),
Type::TEXT | Type::VARCHAR => i.to_string().to_sql(&Type::TEXT, out),
_ => Err(format!("cannot bind Integer to {ty}").into()),
},
Value::Real(r) => match *ty {
Type::FLOAT8 => r.to_sql(ty, out),
Type::FLOAT4 => (*r as f32).to_sql(&Type::FLOAT4, out),
_ => Err(format!("cannot bind Real to {ty}").into()),
},
Value::Text(s) => match *ty {
Type::TEXT | Type::VARCHAR | Type::NAME => s.to_sql(&Type::TEXT, out),
_ => Err(format!("cannot bind Text to {ty}").into()),
},
Value::Blob(b) => match *ty {
Type::BYTEA => b.to_sql(ty, out),
_ => Err(format!("cannot bind Blob to {ty}").into()),
},
}
}
fn accepts(_ty: &Type) -> bool {
true }
tokio_postgres::types::to_sql_checked!();
}
fn row_to_row(row: &tokio_postgres::Row) -> Result<Row> {
let mut vals = Vec::with_capacity(row.len());
for (i, col) in row.columns().iter().enumerate() {
let v = match *col.type_() {
Type::INT8 => row
.try_get::<_, Option<i64>>(i)
.map(|o| o.map_or(Value::Null, Value::Integer)),
Type::INT4 => row
.try_get::<_, Option<i32>>(i)
.map(|o| o.map_or(Value::Null, |x| Value::Integer(x as i64))),
Type::INT2 => row
.try_get::<_, Option<i16>>(i)
.map(|o| o.map_or(Value::Null, |x| Value::Integer(x as i64))),
Type::FLOAT8 => row
.try_get::<_, Option<f64>>(i)
.map(|o| o.map_or(Value::Null, Value::Real)),
Type::FLOAT4 => row
.try_get::<_, Option<f32>>(i)
.map(|o| o.map_or(Value::Null, |x| Value::Real(x as f64))),
Type::TEXT | Type::VARCHAR | Type::NAME | Type::UNKNOWN => row
.try_get::<_, Option<String>>(i)
.map(|o| o.map_or(Value::Null, Value::Text)),
Type::BYTEA => row
.try_get::<_, Option<Vec<u8>>>(i)
.map(|o| o.map_or(Value::Null, Value::Blob)),
Type::BOOL => row
.try_get::<_, Option<bool>>(i)
.map(|o| o.map_or(Value::Null, |b| Value::Integer(b as i64))),
ref other => {
return Err(AreevError::Storage(format!(
"unmapped postgres column type {other} at index {i} — extend row_to_row"
)))
}
}
.map_err(pg_err)?;
vals.push(v);
}
Ok(Row(vals))
}
fn replace_suffix(table: &str) -> Option<(&'static str, &'static str)> {
match table {
"meta" => Some(("(k)", "v = EXCLUDED.v")),
"fts_doc" => Some(("(seq)", "len = EXCLUDED.len")),
"entity_latest" => {
Some(("(ns,s,p)", "o = EXCLUDED.o, seq = EXCLUDED.seq, hash = EXCLUDED.hash"))
}
"heads" => Some(("(ns,s,p,seq)", "hash = EXCLUDED.hash, created_at = EXCLUDED.created_at")),
"telem_meta" => Some(("(k)", "v = EXCLUDED.v")),
_ => None,
}
}
pub const PG_TABLES: &[&str] = &[
"blobs", "corpus_idx", "counters", "embeddings", "entity_latest", "fts_doc", "fts_post",
"fts_vocab", "grains", "heads", "meta", "ns_reg", "oplog", "osp", "prov_idx", "run_idx",
"telem_budget_stat", "telem_grain_access", "telem_meta", "telem_query_stat",
"telem_recall_log", "terms", "thread_idx", "triples",
];
pub(crate) fn qualify_tables(sql: &str, layout: &PgLayout) -> String {
const TRIGGERS: &[&str] = &["FROM", "JOIN", "INTO", "UPDATE", "TABLE", "EXISTS", "ON"];
let mut out = String::with_capacity(sql.len() + 64);
let chars: Vec<char> = sql.chars().collect();
let n = chars.len();
let mut i = 0;
let mut in_str = false;
let mut in_ident = false;
let mut prev_word_is_trigger = false;
let is_word = |c: char| c.is_ascii_alphanumeric() || c == '_';
while i < n {
let c = chars[i];
if in_str {
out.push(c);
if c == '\'' {
in_str = false;
}
i += 1;
continue;
}
if in_ident {
out.push(c);
if c == '"' {
in_ident = false;
}
i += 1;
continue;
}
match c {
'\'' => {
in_str = true;
prev_word_is_trigger = false;
out.push(c);
i += 1;
}
'"' => {
in_ident = true;
prev_word_is_trigger = false;
out.push(c);
i += 1;
}
c if is_word(c) => {
let start = i;
while i < n && is_word(chars[i]) {
i += 1;
}
let word: String = chars[start..i].iter().collect();
let preceded_by_dot = start > 0 && chars[start - 1] == '.';
if prev_word_is_trigger && !preceded_by_dot && PG_TABLES.contains(&word.as_str()) {
out.push('"');
out.push_str(layout.schema_for(&word));
out.push_str("\".");
}
out.push_str(&word);
prev_word_is_trigger = TRIGGERS.contains(&word.to_ascii_uppercase().as_str());
}
c if c.is_whitespace() => {
out.push(c);
i += 1;
}
_ => {
prev_word_is_trigger = false;
out.push(c);
i += 1;
}
}
}
out
}
pub(crate) fn translate(sql: &str) -> Result<String> {
let trimmed = sql.trim();
if trimmed == "PRAGMA integrity_check" {
return Ok("SELECT * FROM (\
SELECT 'heads rows without a grain: ' || count(*)::text AS line \
FROM heads h LEFT JOIN grains g ON g.seq = h.seq \
WHERE g.seq IS NULL HAVING count(*) > 0 \
UNION ALL \
SELECT 'entity_latest rows without a grain: ' || count(*)::text \
FROM entity_latest e LEFT JOIN grains g ON g.seq = e.seq \
WHERE g.seq IS NULL HAVING count(*) > 0 \
UNION ALL \
SELECT 'triples rows without a grain: ' || count(*)::text \
FROM triples t LEFT JOIN grains g ON g.seq = t.seq \
WHERE g.seq IS NULL HAVING count(*) > 0 \
UNION ALL \
SELECT 'osp rows without a grain: ' || count(*)::text \
FROM osp o LEFT JOIN grains g ON g.seq = o.seq \
WHERE g.seq IS NULL HAVING count(*) > 0 \
) checks"
.into());
}
if trimmed == "DROP INDEX idx_fts" {
return Ok("DROP INDEX IF EXISTS idx_fts".into());
}
if trimmed == "SELECT COALESCE(SUM(len),0) FROM fts_doc" {
return Ok("SELECT COALESCE(SUM(len),0)::bigint FROM fts_doc".into());
}
if trimmed.starts_with("INSERT OR REPLACE INTO telem_grain_access") {
return Ok("INSERT INTO telem_grain_access(hash, ns, recall_count, first_ms, last_ms) \
VALUES ($1, $2, 1, $3, $3) \
ON CONFLICT (hash) DO UPDATE SET \
recall_count = telem_grain_access.recall_count + 1, \
last_ms = EXCLUDED.last_ms, ns = EXCLUDED.ns"
.into());
}
if trimmed.starts_with("INSERT OR REPLACE INTO telem_query_stat") {
return Ok("INSERT INTO telem_query_stat(qkey, ns, sample, run_count, last_ms, empty_count, sum_results) \
VALUES ($1, $2, $3, 1, $4, $5, $6) \
ON CONFLICT (qkey) DO UPDATE SET \
run_count = telem_query_stat.run_count + 1, \
last_ms = EXCLUDED.last_ms, \
empty_count = telem_query_stat.empty_count + EXCLUDED.empty_count, \
sum_results = telem_query_stat.sum_results + EXCLUDED.sum_results, \
sample = EXCLUDED.sample, ns = EXCLUDED.ns"
.into());
}
if trimmed.starts_with("INSERT OR REPLACE INTO telem_budget_stat") {
return Ok("INSERT INTO telem_budget_stat(id, sample_count, overflow_count) \
VALUES (1, 1, $1) \
ON CONFLICT (id) DO UPDATE SET \
sample_count = telem_budget_stat.sample_count + 1, \
overflow_count = telem_budget_stat.overflow_count + EXCLUDED.overflow_count"
.into());
}
let mut s = sql.to_string();
if let Some(rest) = s.trim_start().strip_prefix("INSERT OR REPLACE INTO ") {
let table: String =
rest.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').collect();
let (target, set) = replace_suffix(&table).ok_or_else(|| {
AreevError::Storage(format!("no upsert mapping for table {table:?} in: {sql}"))
})?;
s = format!(
"INSERT INTO {rest} ON CONFLICT {target} DO UPDATE SET {set}",
rest = rest.trim_end()
);
} else if let Some(rest) = s.trim_start().strip_prefix("INSERT OR IGNORE INTO ") {
s = format!("INSERT INTO {} ON CONFLICT DO NOTHING", rest.trim_end());
}
while let Some(pos) = s.find("vector_distance_cos(") {
let args_start = pos + "vector_distance_cos(".len();
let (a, b, end) = split_two_args(&s[args_start..]).ok_or_else(|| {
AreevError::Storage(format!("unparseable vector_distance_cos in: {sql}"))
})?;
let repl = format!("({} <=> {})", a.trim(), b.trim());
s = format!("{}{}{}", &s[..pos], repl, &s[args_start + end + 1..]);
}
while let Some(pos) = s.find("vector32(") {
let args_start = pos + "vector32(".len();
let end = s[args_start..]
.find(')')
.ok_or_else(|| AreevError::Storage(format!("unparseable vector32 in: {sql}")))?;
let inner = s[args_start..args_start + end].to_string();
s = format!("{}(({})::text::vector){}", &s[..pos], inner, &s[args_start + end + 1..]);
}
let mut out = String::with_capacity(s.len());
let mut in_str = false;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\'' => {
in_str = !in_str;
out.push(c);
}
'?' if !in_str && chars.peek().is_some_and(|n| n.is_ascii_digit()) => out.push('$'),
_ => out.push(c),
}
}
for marker in ["INSERT OR REPLACE", "INSERT OR IGNORE", "PRAGMA", "vector32", "vector_distance_cos"] {
if out.contains(marker) {
return Err(AreevError::Storage(format!(
"untranslated {marker} construct in: {sql}"
)));
}
}
Ok(out)
}
fn split_two_args(s: &str) -> Option<(&str, &str, usize)> {
let mut depth = 0usize;
let mut comma = None;
for (i, c) in s.char_indices() {
match c {
'(' => depth += 1,
')' => {
if depth == 0 {
let comma = comma?;
return Some((&s[..comma], &s[comma + 1..i], i));
}
depth -= 1;
}
',' if depth == 0 && comma.is_none() => comma = Some(i),
_ => {}
}
}
None
}
pub fn split_schema_url(url: &str) -> Result<(String, String)> {
let Some((base, query)) = url.split_once('?') else {
return Err(AreevError::Validation(
"postgres URL needs ?schema=<name> — one memory = one schema, so the schema \
must be named explicitly"
.into(),
));
};
let mut schema = None;
let mut rest: Vec<&str> = Vec::new();
for pair in query.split('&') {
match pair.split_once('=') {
Some(("schema", v)) if !v.is_empty() => schema = Some(v.to_string()),
_ => rest.push(pair),
}
}
let schema = schema.ok_or_else(|| {
AreevError::Validation("postgres URL needs ?schema=<name>".into())
})?;
let url = if rest.is_empty() {
base.to_string()
} else {
format!("{base}?{}", rest.join("&"))
};
Ok((url, schema))
}
fn term_hash(term: &str) -> Vec<u8> {
use sha2::{Digest, Sha256};
Sha256::digest(term.as_bytes()).to_vec()
}
#[cfg(feature = "conformance")]
pub fn execute_raw(url: &str, sql: &str) -> Result<()> {
let (pool, _) = PgPool::for_url(url)?;
let mut co = pool.checkout()?;
let r = pool.block_on(co.client().batch_execute(sql)).map_err(pg_err);
if let Err(e) = &r {
co.note(e);
}
r
}
#[cfg(feature = "conformance")]
pub fn query_raw_i64(url: &str, sql: &str) -> Result<i64> {
let (pool, _) = PgPool::for_url(url)?;
let mut co = pool.checkout()?;
let r = pool.block_on(co.client().query_one(sql, &[])).map_err(pg_err);
if let Err(e) = &r {
co.note(e);
}
Ok(r?.get::<_, i64>(0))
}
fn session_chaos_mode() -> Option<String> {
#[cfg(feature = "conformance")]
{
std::env::var("AREEV_PG_SESSION_CHAOS").ok().filter(|v| !v.is_empty() && v != "0")
}
#[cfg(not(feature = "conformance"))]
{
None
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StampState {
pub name: String,
pub found: Option<String>,
pub wanted: String,
}
impl StampState {
pub fn is_current(&self) -> bool {
self.found.as_deref() == Some(self.wanted.as_str())
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ProvisionReport {
pub schema: String,
pub meta_schema: Option<String>,
pub exists: bool,
pub stamps: Vec<StampState>,
pub pending: Vec<String>,
pub rolling_deploy: String,
}
impl ProvisionReport {
pub fn is_current(&self) -> bool {
self.exists && self.pending.is_empty()
}
}
pub fn check_provision(
url: &str,
schema: &str,
telemetry: crate::TelemetryMode,
) -> Result<ProvisionReport> {
let layout = PgLayout::from_url(url, schema)?;
let (pool, _) = PgPool::for_url(url)?;
let co = pool.checkout()?;
let exists = pool.block_on(PgDb::schemas_exist(co.client(), &layout))?;
let mut stamps = Vec::new();
let mut wanted: Vec<(&str, &str, &str, &str)> = vec![
("pg_schema", "meta", "pg_schema", PG_SCHEMA_VERSION),
(
"link_index",
"meta",
crate::LINK_INDEX_KEY,
crate::LINK_INDEX_VERSION,
),
(
"ns_registry",
"meta",
crate::NS_REGISTRY_KEY,
crate::NS_REGISTRY_VERSION,
),
];
if !matches!(telemetry, crate::TelemetryMode::Off) {
wanted.push((
"telem_schema",
"telem_meta",
"schema_version",
crate::telemetry::TELEM_SCHEMA_VERSION,
));
}
for (name, table, key, want) in wanted {
let found = if exists {
pool.block_on(PgDb::schema_stamp(
co.client(),
&layout,
PgStamp { table, key, version: want },
))?
} else {
None
};
stamps.push(StampState {
name: name.to_string(),
found,
wanted: want.to_string(),
});
}
let pending: Vec<String> = stamps
.iter()
.filter(|s| !s.is_current())
.map(|s| s.name.clone())
.collect();
let store_found = stamps
.iter()
.find(|s| s.name == "pg_schema")
.and_then(|s| s.found.clone());
let rolling_deploy = match store_found.as_deref() {
None => "unknown",
Some(v) if version_lt(v, PG_ROLLING_SAFE_FROM) => "drain_writers_first",
Some(_) => "safe",
}
.to_string();
Ok(ProvisionReport {
schema: schema.to_string(),
meta_schema: layout.meta_schema().map(str::to_string),
exists,
stamps,
pending,
rolling_deploy,
})
}
fn version_lt(a: &str, b: &str) -> bool {
match (a.parse::<u64>(), b.parse::<u64>()) {
(Ok(x), Ok(y)) => x < y,
_ => a != b,
}
}
pub fn drop_postgres_schema(url: &str, schema: &str) -> Result<()> {
drop_postgres_schema_with(url, schema, &DropOptions::default())
}
#[derive(Debug, Clone, Default)]
pub struct DropOptions {
pub override_hold: Option<crate::HoldOverride>,
}
pub fn drop_postgres_schema_with(
url: &str,
schema: &str,
opts: &DropOptions,
) -> Result<()> {
let layout = PgLayout::from_url(url, schema)?;
let (pool, _) = PgPool::for_url(url)?;
let mut co = pool.checkout()?;
if opts.override_hold.is_none() {
let probe = pool.block_on(co.client().query(
&format!(
"SELECT k FROM {} WHERE k LIKE 'hold:%' ORDER BY k",
layout.qualified("meta")
),
&[],
));
if let Ok(rows) = probe {
if !rows.is_empty() {
let held: Vec<String> = rows
.iter()
.filter_map(|r| r.try_get::<_, String>(0).ok())
.map(|k| k.trim_start_matches("hold:").to_string())
.collect();
return Err(AreevError::LegalHold(format!(
"schema {schema:?} carries live legal holds on {} — dropping it \
would destroy held records; release the holds or drop with an \
explicit override",
held.join(", ")
)));
}
}
}
let sql = layout
.schemas()
.iter()
.map(|s| format!("DROP SCHEMA IF EXISTS \"{s}\" CASCADE"))
.collect::<Vec<_>>()
.join("; ");
let r = pool.block_on(co.client().batch_execute(&sql)).map_err(pg_err);
if let Err(e) = &r {
co.note(e);
}
r
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn translates_placeholders() {
assert_eq!(
translate("SELECT seq FROM triples WHERE ns=?1 AND s=?2 LIMIT ?3").unwrap(),
"SELECT seq FROM triples WHERE ns=$1 AND s=$2 LIMIT $3"
);
assert_eq!(translate("SELECT '?' FROM meta").unwrap(), "SELECT '?' FROM meta");
assert_eq!(
translate("SELECT k FROM meta WHERE v = '?1' AND k = ?1").unwrap(),
"SELECT k FROM meta WHERE v = '?1' AND k = $1"
);
assert_eq!(
translate("SELECT 'café ''?2'' naïve' FROM meta WHERE k = ?1").unwrap(),
"SELECT 'café ''?2'' naïve' FROM meta WHERE k = $1"
);
}
#[test]
fn integrity_check_maps_to_referential_invariants() {
let q = translate("PRAGMA integrity_check").unwrap();
for frag in ["heads rows without a grain", "entity_latest rows", "triples rows", "osp rows"] {
assert!(q.contains(frag), "{q}");
}
}
#[test]
fn translates_upserts_per_table() {
assert_eq!(
translate("INSERT OR REPLACE INTO meta(k, v) VALUES (?1, ?2)").unwrap(),
"INSERT INTO meta(k, v) VALUES ($1, $2) ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v"
);
let el = translate(
"INSERT OR REPLACE INTO entity_latest(ns,s,p,o,seq,hash) VALUES (?1,?2,?3,?4,?5,?6)",
)
.unwrap();
assert!(el.ends_with(
"ON CONFLICT (ns,s,p) DO UPDATE SET o = EXCLUDED.o, seq = EXCLUDED.seq, hash = EXCLUDED.hash"
), "{el}");
let heads = translate(
"INSERT OR REPLACE INTO heads(ns,s,p,seq,hash,created_at) VALUES (?1,?2,?3,?4,?5,?6)",
)
.unwrap();
assert!(heads.contains("ON CONFLICT (ns,s,p,seq) DO UPDATE"), "{heads}");
assert!(translate("INSERT OR REPLACE INTO nope(x) VALUES (?1)").is_err());
}
#[test]
fn translates_or_ignore_and_pragma() {
assert_eq!(
translate("INSERT OR IGNORE INTO fts_vocab(term) VALUES (?1)").unwrap(),
"INSERT INTO fts_vocab(term) VALUES ($1) ON CONFLICT DO NOTHING"
);
assert!(translate("PRAGMA integrity_check").unwrap().starts_with("SELECT * FROM ("));
assert_eq!(
translate("SELECT COALESCE(SUM(len),0) FROM fts_doc").unwrap(),
"SELECT COALESCE(SUM(len),0)::bigint FROM fts_doc"
);
assert_eq!(
translate("INSERT OR IGNORE INTO ns_reg(ns, n) VALUES (?1, 0)").unwrap(),
"INSERT INTO ns_reg(ns, n) VALUES ($1, 0) ON CONFLICT DO NOTHING"
);
assert_eq!(
translate("UPDATE ns_reg SET n = n + ?2 WHERE ns = ?1").unwrap(),
"UPDATE ns_reg SET n = n + $2 WHERE ns = $1"
);
assert_eq!(
translate("DELETE FROM ns_reg WHERE ns = ?1 AND n <= 0").unwrap(),
"DELETE FROM ns_reg WHERE ns = $1 AND n <= 0"
);
}
#[test]
fn translates_vector_functions() {
assert_eq!(
translate("INSERT INTO embeddings(seq, vec) VALUES (?1, vector32(?2))").unwrap(),
"INSERT INTO embeddings(seq, vec) VALUES ($1, (($2)::text::vector))"
);
let q = translate(
"SELECT e.seq FROM embeddings e JOIN grains g ON g.seq=e.seq WHERE g.ns=?1 AND g.svt IS NULL ORDER BY vector_distance_cos(e.vec, vector32(?2)) LIMIT ?3",
)
.unwrap();
assert!(
q.contains("ORDER BY (e.vec <=> (($2)::text::vector)) LIMIT $3"),
"{q}"
);
let p = translate(
"SELECT a.seq, b.seq, vector_distance_cos(a.vec, b.vec) FROM embeddings a JOIN embeddings b ON a.seq < b.seq",
)
.unwrap();
assert!(p.contains("(a.vec <=> b.vec)"), "{p}");
let n = translate(
"SELECT g.hash, vector_distance_cos(e.vec, vector32(?2)) AS dist FROM embeddings e JOIN grains g ON g.seq=e.seq WHERE g.ns=?1 ORDER BY dist LIMIT ?3",
)
.unwrap();
assert!(n.contains("(e.vec <=> (($2)::text::vector)) AS dist"), "{n}");
}
#[test]
fn a_stale_statement_error_names_both_remedies() {
let e = PgDb::explain_stale(AreevError::Storage(
"postgres error 26000: prepared statement \"s7\" does not exist".into(),
));
let msg = e.to_string();
assert!(msg.contains("26000") && msg.contains("max_prepared_statements"), "{msg}");
assert!(msg.contains("session mode"), "{msg}");
let other = PgDb::explain_stale(AreevError::Storage("postgres error 42P01: relation".into()));
assert_eq!(other.to_string(), "STO-E001: storage error: postgres error 42P01: relation");
}
#[test]
fn qualifies_every_table_reference_and_nothing_else() {
let single = PgLayout::single("s1").unwrap();
let q = |sql: &str| qualify_tables(sql, &single);
assert_eq!(
q("SELECT g.hash FROM embeddings e JOIN grains g ON g.seq = e.seq WHERE g.ns = $1"),
"SELECT g.hash FROM \"s1\".embeddings e JOIN \"s1\".grains g ON g.seq = e.seq WHERE g.ns = $1"
);
assert_eq!(
q("INSERT INTO terms(term, term_hash) VALUES ($1, $2) ON CONFLICT (term_hash) DO NOTHING RETURNING id"),
"INSERT INTO \"s1\".terms(term, term_hash) VALUES ($1, $2) ON CONFLICT (term_hash) DO NOTHING RETURNING id"
);
assert_eq!(
q("DELETE FROM run_idx WHERE seq=$1 AND NOT EXISTS (SELECT 1 FROM triples WHERE s=$1)"),
"DELETE FROM \"s1\".run_idx WHERE seq=$1 AND NOT EXISTS (SELECT 1 FROM \"s1\".triples WHERE s=$1)"
);
assert_eq!(q("UPDATE terms SET term = $2 WHERE id = $1"), "UPDATE \"s1\".terms SET term = $2 WHERE id = $1");
assert_eq!(
q("ALTER TABLE telem_recall_log ADD COLUMN run_id TEXT"),
"ALTER TABLE \"s1\".telem_recall_log ADD COLUMN run_id TEXT"
);
assert_eq!(q("DROP TABLE IF EXISTS meta"), "DROP TABLE IF EXISTS \"s1\".meta");
assert_eq!(
q("CREATE INDEX IF NOT EXISTS idx_grains_hash ON grains(hash)"),
"CREATE INDEX IF NOT EXISTS idx_grains_hash ON \"s1\".grains(hash)"
);
assert_eq!(
q("SELECT 1 FROM heads h LEFT JOIN grains g ON g.seq = h.seq"),
"SELECT 1 FROM \"s1\".heads h LEFT JOIN \"s1\".grains g ON g.seq = h.seq"
);
}
#[test]
fn qualifier_routes_metadata_tables_to_the_metadata_schema() {
let pair = PgLayout::paired("s1", "m1").unwrap();
let q = |sql: &str| qualify_tables(sql, &pair);
assert_eq!(q("SELECT v FROM meta WHERE k = $1"), "SELECT v FROM \"m1\".meta WHERE k = $1");
assert_eq!(
q("UPDATE counters SET v = v + $1 WHERE name = 'seq' RETURNING name, v"),
"UPDATE \"m1\".counters SET v = v + $1 WHERE name = 'seq' RETURNING name, v"
);
assert_eq!(q("DELETE FROM ns_reg WHERE ns = $1"), "DELETE FROM \"m1\".ns_reg WHERE ns = $1");
assert_eq!(
q("INSERT INTO counters(name, v) SELECT 'seq', COALESCE(MAX(seq),0) FROM grains \
ON CONFLICT (name) DO UPDATE SET v = GREATEST(counters.v, EXCLUDED.v)"),
"INSERT INTO \"m1\".counters(name, v) SELECT 'seq', COALESCE(MAX(seq),0) FROM \"s1\".grains \
ON CONFLICT (name) DO UPDATE SET v = GREATEST(counters.v, EXCLUDED.v)"
);
assert_eq!(
q("CREATE INDEX IF NOT EXISTS idx_telem_recall_ts ON telem_recall_log(ts_ms)"),
"CREATE INDEX IF NOT EXISTS idx_telem_recall_ts ON \"m1\".telem_recall_log(ts_ms)"
);
assert_eq!(
q("CREATE TABLE IF NOT EXISTS telem_meta(k text PRIMARY KEY, v text)"),
"CREATE TABLE IF NOT EXISTS \"m1\".telem_meta(k text PRIMARY KEY, v text)"
);
assert_eq!(
q("SELECT body FROM blobs WHERE hash = $1"),
"SELECT body FROM \"s1\".blobs WHERE hash = $1"
);
for sql in PG_SCHEMA.iter().chain(PG_SEED.iter()).chain(crate::telemetry::TELEM_SCHEMA_PG) {
let routed = q(sql);
for t in META_TABLES {
assert!(
!routed.contains(&format!("\"s1\".{t}")),
"metadata table {t} routed to the memory schema in: {routed}"
);
}
}
}
#[test]
fn layout_reads_the_dsn_and_refuses_a_degenerate_pair() {
let single = PgLayout::from_url("postgres://h/db?pool=2", "s").unwrap();
assert!(!single.is_paired());
assert_eq!(single.meta_schema(), None);
assert_eq!(single.schemas(), vec!["s"]);
assert_eq!(single.schema_for("meta"), "s");
let pair = PgLayout::from_url("postgres://h/db?meta_schema=s_meta&sslmode=require", "s").unwrap();
assert!(pair.is_paired());
assert_eq!(pair.meta_schema(), Some("s_meta"));
assert_eq!(pair.schemas(), vec!["s", "s_meta"]);
for t in META_TABLES {
assert_eq!(pair.schema_for(t), "s_meta", "{t}");
}
for t in PG_TABLES.iter().filter(|t| !META_TABLES.contains(t)) {
assert_eq!(pair.schema_for(t), "s", "{t}");
}
assert_eq!(pair.qualified("counters"), "\"s_meta\".counters");
assert_eq!(pair.qualified("grains"), "\"s\".grains");
let e = PgLayout::from_url("postgres://h/db?meta_schema=s", "s").unwrap_err();
assert!(e.to_string().contains("names the memory schema itself"), "{e}");
let e = PgLayout::from_url("postgres://h/db?meta_schema=", "s").unwrap_err();
assert!(e.to_string().contains("names no schema"), "{e}");
assert!(PgLayout::paired("s", "has space").is_err());
assert!(PgLayout::paired("9lead", "m").is_err());
assert_eq!(
strip_meta_schema("postgres://h/db?schema=s&meta_schema=m&provision=never"),
"postgres://h/db?schema=s&provision=never"
);
let req = crate::pgtls::SslRequest::split("postgres://u@h/db?meta_schema=m&sslmode=disable")
.unwrap();
assert_eq!(req.dsn, "postgres://u@h/db");
assert_eq!(pool_key("postgres://u@h/db?schema=s&meta_schema=m&pool=3"), "postgres://u@h/db");
}
#[test]
fn qualifier_leaves_data_catalogs_and_target_references_alone() {
let single = PgLayout::single("s1").unwrap();
let q = |sql: &str| qualify_tables(sql, &single);
assert_eq!(q("SELECT k FROM meta WHERE v = 'FROM grains'"), "SELECT k FROM \"s1\".meta WHERE v = 'FROM grains'");
let cat = "SELECT 1 FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid";
assert_eq!(q(cat), cat);
let info = "SELECT 1 FROM information_schema.columns WHERE table_name = 'terms'";
assert_eq!(q(info), info);
assert_eq!(
q("INSERT INTO counters(name, v) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET v = GREATEST(counters.v, EXCLUDED.v)"),
"INSERT INTO \"s1\".counters(name, v) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET v = GREATEST(counters.v, EXCLUDED.v)"
);
assert_eq!(q("SELECT seq FROM grains WHERE hash=$1 FOR UPDATE"), "SELECT seq FROM \"s1\".grains WHERE hash=$1 FOR UPDATE");
let done = "SELECT 1 FROM \"s1\".grains g WHERE g.meta = 1";
assert_eq!(q(done), done);
assert_eq!(q(&q("SELECT 1 FROM heads")), "SELECT 1 FROM \"s1\".heads");
}
#[test]
fn every_translated_special_qualifies_cleanly() {
let single = PgLayout::single("s1").unwrap();
let out = qualify_tables(&translate("PRAGMA integrity_check").unwrap(), &single);
for t in ["heads", "grains", "entity_latest", "triples"] {
assert!(out.contains(&format!("\"s1\".{t}")), "{t} unqualified in: {out}");
}
assert!(!out.contains(" heads h") || out.contains("\"s1\".heads h"), "{out}");
}
#[test]
fn every_pg_schema_statement_is_plain_ddl() {
for sql in PG_SCHEMA {
assert_eq!(&translate(sql).unwrap(), sql);
}
}
#[test]
fn splits_schema_urls() {
let (u, s) =
split_schema_url("postgres://u:p@h:5432/db?schema=org_7").unwrap();
assert_eq!(u, "postgres://u:p@h:5432/db");
assert_eq!(s, "org_7");
let (u, s) = split_schema_url(
"postgres://u:p@h/db?sslmode=disable&schema=m1&connect_timeout=5",
)
.unwrap();
assert_eq!(u, "postgres://u:p@h/db?sslmode=disable&connect_timeout=5");
assert_eq!(s, "m1");
assert!(split_schema_url("postgres://u:p@h/db").is_err());
assert!(split_schema_url("postgres://u:p@h/db?schema=").is_err());
}
#[test]
fn schema_name_validation() {
for bad in ["", "has space", "semi;colon", "quote\"", "9leading", &"x".repeat(64)] {
assert!(
PgDb::open("postgres://ignored", bad, &[], false, None).is_err(),
"{bad:?} must be rejected"
);
}
}
}
#[cfg(test)]
mod schema_version_tests {
#[test]
fn rolling_deploy_verdict_reads_the_found_stamp() {
assert!(version_lt("1", "2"));
assert!(!version_lt("2", "2"));
assert!(!version_lt("3", "2"));
assert!(version_lt("wat", "2"));
}
#[test]
fn this_builds_migration_is_rolling_safe_from_the_stated_version() {
assert!(!version_lt(PG_ROLLING_SAFE_FROM, PG_SCHEMA_VERSION)
|| PG_ROLLING_SAFE_FROM == "1");
}
use super::*;
const EXPECTED_DIGEST: &str =
"6e0120dfa4c54bdbc8e1beaaf402719df728d0bda1fffb628881458cc39f541f";
fn digest() -> String {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
for sql in PG_SCHEMA.iter().chain(PG_SEED.iter()) {
h.update(sql.as_bytes());
h.update([0u8]);
}
hex::encode(h.finalize())
}
#[test]
fn pg_schema_version_tracks_the_schema() {
assert_eq!(
digest(),
EXPECTED_DIGEST,
"PG_SCHEMA/PG_SEED changed. Bump PG_SCHEMA_VERSION (currently {PG_SCHEMA_VERSION:?}) \
and update EXPECTED_DIGEST to the value above — a schema stamped with an unbumped \
version silently skips the migration it needs"
);
}
#[test]
fn pool_size_reads_the_dsn_then_the_environment() {
assert_eq!(pool_size("postgres://u@h/db?schema=x").unwrap(), DEFAULT_POOL_SIZE);
assert_eq!(pool_size("postgres://u@h/db?pool=3&schema=x").unwrap(), 3);
assert!(pool_size("postgres://u@h/db?pool=0").is_err());
assert!(pool_size("postgres://u@h/db?pool=many").is_err());
std::env::set_var(POOL_ENV, "5");
assert_eq!(pool_size("postgres://u@h/db").unwrap(), 5);
assert_eq!(pool_size("postgres://u@h/db?pool=2").unwrap(), 2, "the DSN wins");
std::env::set_var(POOL_ENV, "x");
assert!(pool_size("postgres://u@h/db").is_err());
std::env::remove_var(POOL_ENV);
assert_eq!(strip_pool("postgres://u@h/db?pool=3&schema=x"), "postgres://u@h/db?schema=x");
let req = crate::pgtls::SslRequest::split("postgres://u@h/db?pool=3&sslmode=disable").unwrap();
assert_eq!(req.dsn, "postgres://u@h/db");
}
#[test]
fn pool_idle_ttl_reads_the_dsn_then_the_environment() {
let secs = |u: &str| pool_idle_ttl(u).unwrap().map(|d| d.as_secs());
assert_eq!(secs("postgres://u@h/db?schema=x"), Some(DEFAULT_POOL_IDLE_SECS));
assert_eq!(secs("postgres://u@h/db?pool_idle_secs=30&schema=x"), Some(30));
assert_eq!(secs("postgres://u@h/db?pool_idle_secs=0"), None);
assert!(pool_idle_ttl("postgres://u@h/db?pool_idle_secs=soon").is_err());
std::env::set_var(POOL_IDLE_ENV, "45");
assert_eq!(secs("postgres://u@h/db"), Some(45));
assert_eq!(secs("postgres://u@h/db?pool_idle_secs=10"), Some(10), "the DSN wins");
std::env::set_var(POOL_IDLE_ENV, "whenever");
assert!(pool_idle_ttl("postgres://u@h/db").is_err());
std::env::remove_var(POOL_IDLE_ENV);
assert_eq!(
strip_pool_idle("postgres://u@h/db?pool_idle_secs=30&schema=x"),
"postgres://u@h/db?schema=x"
);
let req =
crate::pgtls::SslRequest::split("postgres://u@h/db?pool_idle_secs=30&sslmode=disable")
.unwrap();
assert_eq!(req.dsn, "postgres://u@h/db");
}
#[test]
fn the_pool_key_ignores_the_idle_parameter_and_a_later_open_is_told_not_obeyed() {
let host = "pool-key.invalid";
let (first, w) =
PgPool::for_url(&format!("postgres://u@{host}/db?pool=4&pool_idle_secs=0&schema=one"))
.unwrap();
assert!(w.is_none(), "{w:?}");
let (second, w) =
PgPool::for_url(&format!("postgres://u@{host}/db?pool=4&pool_idle_secs=0&schema=two"))
.unwrap();
assert!(w.is_none(), "{w:?}");
assert!(std::sync::Arc::ptr_eq(&first, &second), "neither schema nor TTL keys the pool");
let (third, w) =
PgPool::for_url(&format!("postgres://u@{host}/db?pool=9&pool_idle_secs=30&schema=c"))
.unwrap();
assert!(std::sync::Arc::ptr_eq(&first, &third));
assert_eq!(third.idle_ttl, None, "the first open set the TTL");
let w = w.unwrap();
assert!(w.contains("sized at 4"), "{w}");
assert!(w.contains("after never"), "{w}");
}
#[test]
fn a_pool_that_holds_nothing_and_that_nothing_holds_is_evicted() {
let host = "pool-evict.invalid";
let (p, _) =
PgPool::for_url(&format!("postgres://u@{host}/db?pool=2&pool_idle_secs=1")).unwrap();
assert!(registry().pools.keys().any(|k| k.contains(host)));
drop(p);
sweep();
assert!(
!registry().pools.keys().any(|k| k.contains(host)),
"an unreferenced, empty pool must not survive a sweep"
);
}
#[test]
fn provision_mode_reads_the_dsn() {
assert_eq!(provision_mode("postgres://h/db").unwrap(), ProvisionMode::Auto);
assert_eq!(
provision_mode("postgres://h/db?sslmode=require").unwrap(),
ProvisionMode::Auto
);
assert_eq!(provision_mode("postgres://h/db?provision=auto").unwrap(), ProvisionMode::Auto);
assert_eq!(
provision_mode("postgres://h/db?schema=s&provision=never").unwrap(),
ProvisionMode::Never
);
let e = provision_mode("postgres://h/db?provision=maybe").unwrap_err();
assert!(e.to_string().contains("auto|never"), "{e}");
}
#[test]
fn strip_provision_leaves_every_other_parameter() {
assert_eq!(
strip_provision("postgres://h/db?schema=s&provision=never&sslmode=require"),
"postgres://h/db?schema=s&sslmode=require"
);
assert_eq!(strip_provision("postgres://h/db?provision=never"), "postgres://h/db");
assert_eq!(strip_provision("postgres://h/db"), "postgres://h/db");
}
}