mod admin_sql;
mod composed;
mod database;
mod numeric;
mod query_stats;
mod relay;
mod streaming;
#[cfg(feature = "test-postgres")]
pub mod test_hooks;
#[cfg(test)]
mod composed_tests;
#[cfg(test)]
mod tests;
use std::{
fmt::Write,
sync::{
Arc, Weak,
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
},
time::Duration,
};
use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime};
use fraiseql_error::{FraiseQLError, Result};
use tokio::sync::Semaphore;
use tokio_postgres::{NoTls, Row};
use super::{
server_version::require_supported_server,
tls::{PostgresConnector, PostgresSslMode, PostgresTlsConfig},
where_generator::PostgresWhereGenerator,
};
use crate::{
dialect::PostgresDialect,
identifier::quote_postgres_identifier,
order_by::{Tiebreak, append_order_by},
postgres::pg_detail,
traits::DatabaseAdapter,
types::{
DatabaseType, JsonbValue, QueryParam, ReadRouting,
sql_hints::{OrderByClause, SqlProjectionHint},
},
where_clause::WhereClause,
};
const fn streaming_read_slots(configured: Option<usize>, max_size: usize) -> usize {
let requested = match configured {
Some(n) => n,
None => max_size / 4,
};
if requested == 0 { 1 } else { requested }
}
fn jsonb_cell<I>(row: &Row, column: I, sql: &str) -> Result<JsonbValue>
where
I: tokio_postgres::row::RowIndex + std::fmt::Display,
{
let query_preview = || sql.chars().take(200).collect::<String>();
match row.try_get::<_, Option<serde_json::Value>>(column) {
Ok(Some(value)) => Ok(JsonbValue::new(value)),
Ok(None) => Err(FraiseQLError::Database {
message: format!(
"Query returned a NULL `data` column; the backing view must project a \
non-NULL JSONB `data` value (a view yielding NULL `data`, e.g. via a \
LEFT JOIN, is unsupported). Query: {}",
query_preview()
),
sql_state: None,
}),
Err(e) => Err(FraiseQLError::Database {
message: format!(
"Failed to read the `data` column as JSONB ({e}); the backing view must \
project a JSONB `data` column. Query: {}",
query_preview()
),
sql_state: None,
}),
}
}
const DEFAULT_POOL_SIZE: usize = 25;
const MAX_CONNECTION_RETRIES: u32 = 3;
const CONNECTION_RETRY_DELAY_MS: u64 = 50;
#[derive(Debug, Clone)]
pub struct PoolPrewarmConfig {
pub min_size: usize,
pub max_size: usize,
pub timeout_secs: Option<u64>,
pub search_path: Option<SearchPath>,
pub tls: PostgresTlsConfig,
pub max_streaming_reads: Option<usize>,
pub vector_scan: VectorScanConfig,
pub read_replicas: Option<ReadReplicaConfig>,
}
#[derive(Debug, Clone)]
pub struct ReadReplicaConfig {
pub urls: Vec<String>,
pub pin_after_write: Duration,
pub max_lag: Option<Duration>,
pub health_probe_interval: Duration,
}
#[derive(Debug, Clone)]
pub struct ReadReplicaPolicy {
pub pin_after_write: Duration,
pub max_lag: Option<Duration>,
pub health_probe_interval: Duration,
}
impl Default for ReadReplicaPolicy {
fn default() -> Self {
Self {
pin_after_write: Duration::from_secs(5),
max_lag: None,
health_probe_interval: Duration::from_secs(1),
}
}
}
impl ReadReplicaPolicy {
#[must_use]
pub fn with_urls(&self, urls: Vec<String>) -> Option<ReadReplicaConfig> {
if urls.is_empty() {
return None;
}
Some(ReadReplicaConfig {
urls,
pin_after_write: self.pin_after_write,
max_lag: self.max_lag,
health_probe_interval: self.health_probe_interval,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SearchPath(String);
impl SearchPath {
pub fn new<I, S>(schemas: I) -> Result<Self>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut parts: Vec<String> = Vec::new();
for schema in schemas {
let name = schema.as_ref();
let mut chars = name.chars();
let valid = match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
},
_ => false,
};
if !valid || name.len() > MAX_PG_IDENTIFIER_LEN {
return Err(FraiseQLError::validation(format!(
"Schema name '{name}' is not a bare PostgreSQL identifier and cannot be \
used in a search path"
)));
}
parts.push(name.to_string());
}
if parts.is_empty() {
return Err(FraiseQLError::validation("A search path needs at least one schema"));
}
Ok(Self(parts.join(",")))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for SearchPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
const MAX_PG_IDENTIFIER_LEN: usize = 63;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HnswIterativeScan {
Off,
RelaxedOrder,
#[default]
StrictOrder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IvfflatIterativeScan {
Off,
#[default]
RelaxedOrder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct VectorScanConfig {
pub hnsw: HnswIterativeScan,
pub ivfflat: IvfflatIterativeScan,
pub ef_search: Option<u32>,
}
impl HnswIterativeScan {
#[must_use]
pub const fn as_guc_value(self) -> &'static str {
match self {
Self::Off => "off",
Self::RelaxedOrder => "relaxed_order",
Self::StrictOrder => "strict_order",
}
}
}
impl IvfflatIterativeScan {
#[must_use]
pub const fn as_guc_value(self) -> &'static str {
match self {
Self::Off => "off",
Self::RelaxedOrder => "relaxed_order",
}
}
}
impl VectorScanConfig {
#[must_use]
pub fn startup_fragments(self) -> Vec<String> {
let mut out = Vec::new();
if self.hnsw != HnswIterativeScan::Off {
out.push(format!("-c hnsw.iterative_scan={}", self.hnsw.as_guc_value()));
}
if self.ivfflat != IvfflatIterativeScan::Off {
out.push(format!("-c ivfflat.iterative_scan={}", self.ivfflat.as_guc_value()));
}
if let Some(ef) = self.ef_search {
out.push(format!("-c hnsw.ef_search={ef}"));
}
out
}
}
fn compose_startup_options(
connection_string: &str,
search_path: Option<&SearchPath>,
vector_scan: VectorScanConfig,
) -> Option<String> {
let mut fragments: Vec<String> = Vec::new();
if let Some(path) = search_path {
fragments.push(format!("-c search_path={path}"));
}
fragments.extend(vector_scan.startup_fragments());
if fragments.is_empty() {
return None;
}
let existing = connection_string
.parse::<tokio_postgres::Config>()
.ok()
.and_then(|c| c.get_options().map(str::to_owned))
.unwrap_or_default();
let existing = existing.trim();
if existing.is_empty() {
Some(fragments.join(" "))
} else {
Some(format!("{existing} {}", fragments.join(" ")))
}
}
fn build_pool(
connection_string: &str,
max_size: usize,
timeout_secs: Option<u64>,
search_path: Option<&SearchPath>,
vector_scan: VectorScanConfig,
tls: &PostgresTlsConfig,
) -> Result<Pool> {
let mut cfg = Config::new();
cfg.url = Some(connection_string.to_string());
cfg.manager = Some(ManagerConfig {
recycling_method: RecyclingMethod::Fast,
});
cfg.options = compose_startup_options(connection_string, search_path, vector_scan);
let mut pool_cfg = deadpool_postgres::PoolConfig::new(max_size);
if let Some(secs) = timeout_secs {
let t = Duration::from_secs(secs);
pool_cfg.timeouts.wait = Some(t);
pool_cfg.timeouts.create = Some(t);
}
cfg.pool = Some(pool_cfg);
cfg.ssl_mode = tls.mode.map(|mode| match mode {
PostgresSslMode::Disable => deadpool_postgres::SslMode::Disable,
PostgresSslMode::Prefer => deadpool_postgres::SslMode::Prefer,
PostgresSslMode::Require | PostgresSslMode::VerifyFull => {
deadpool_postgres::SslMode::Require
},
});
let pool = match tls.connector()? {
PostgresConnector::Plaintext => cfg.create_pool(Some(Runtime::Tokio1), NoTls),
PostgresConnector::Tls(connector) => cfg.create_pool(Some(Runtime::Tokio1), *connector),
};
pool.map_err(|e| FraiseQLError::ConnectionPool {
message: format!("Failed to create connection pool: {e}"),
})
}
async fn build_read_replica_set(
rc: &ReadReplicaConfig,
cfg: &PoolPrewarmConfig,
) -> Result<ReadReplicaSet> {
if rc.urls.is_empty() {
return Err(FraiseQLError::validation(
"read_replicas was configured with an empty URL list; remove the configuration \
or list at least one replica",
));
}
if rc.health_probe_interval.is_zero() {
return Err(FraiseQLError::validation(
"read replica health_probe_interval is zero; probing every replica in a tight \
loop is never intended. Set an interval, or remove the replica configuration.",
));
}
if rc.max_lag.is_some_and(|max_lag| max_lag <= rc.health_probe_interval) {
return Err(FraiseQLError::validation(format!(
"read replica max_lag ({} ms) must be greater than health_probe_interval ({} ms): \
a replica's eligibility ages its last probe, so a staleness budget no larger than \
the probe period would drop even a fully caught-up replica out of rotation for \
part of every cycle. Lower the probe interval or raise the budget.",
rc.max_lag.unwrap_or_default().as_millis(),
rc.health_probe_interval.as_millis(),
)));
}
let mut pools = Vec::with_capacity(rc.urls.len());
let mut health = Vec::with_capacity(rc.urls.len());
for (index, url) in rc.urls.iter().enumerate() {
let pool = build_pool(
url,
cfg.max_size,
cfg.timeout_secs,
cfg.search_path.as_ref(),
cfg.vector_scan,
&cfg.tls,
)?;
let client = pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
message: format!(
"Read replica {index} is unreachable at boot ({e}); refusing to start"
),
})?;
let row = client.query_one("SELECT pg_is_in_recovery()", &[]).await.map_err(|e| {
FraiseQLError::Database {
message: format!(
"Read replica {index} failed its boot health check: {}",
pg_detail(&e)
),
sql_state: e.code().map(|c| c.code().to_string()),
}
})?;
let in_recovery: bool = row.get(0);
require_supported_server(&client).await.map_err(|e| match e {
FraiseQLError::Unsupported { message } => FraiseQLError::Unsupported {
message: format!("Read replica {index}: {message}"),
},
other => other,
})?;
if !in_recovery {
tracing::warn!(
replica = index,
"Configured read replica is not in recovery (pg_is_in_recovery() = false); it \
is a writable server, not a standby. Reads routed to it may diverge from the \
primary."
);
}
drop(client);
pools.push(pool);
health.push(ReplicaHealth::booted(in_recovery));
}
let pin_after_write_ms = u64::try_from(rc.pin_after_write.as_millis()).unwrap_or(u64::MAX);
let max_lag_ms = rc.max_lag.map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX));
Ok(ReadReplicaSet {
pools,
health,
next: AtomicUsize::new(0),
pin_after_write_ms,
max_lag_ms,
probe_interval: rc.health_probe_interval,
last_write_unix_ms: AtomicU64::new(0),
})
}
#[derive(Clone)]
pub struct PostgresAdapter {
pub(super) pool: Pool,
read_replicas: Option<Arc<ReadReplicaSet>>,
mutation_timing_enabled: bool,
timing_variable_name: String,
streaming_permits: Arc<Semaphore>,
}
struct ReadReplicaSet {
pools: Vec<Pool>,
health: Vec<ReplicaHealth>,
next: AtomicUsize,
pin_after_write_ms: u64,
max_lag_ms: Option<u64>,
probe_interval: Duration,
last_write_unix_ms: AtomicU64,
}
struct ReplicaHealth {
lag_ms: AtomicU64,
probed_at_unix_ms: AtomicU64,
in_recovery: AtomicBool,
booted_in_recovery: bool,
}
const LAG_UNMEASURABLE: u64 = u64::MAX;
const REPLICA_PROBE_SQL: &str = "SELECT pg_is_in_recovery(), \
CASE \
WHEN NOT pg_is_in_recovery() THEN NULL::bigint \
WHEN pg_last_wal_replay_lsn() IS NOT NULL \
AND pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn() THEN 0::bigint \
WHEN pg_last_xact_replay_timestamp() IS NULL THEN NULL::bigint \
ELSE GREATEST(0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) * 1000)::bigint \
END";
impl ReplicaHealth {
const fn booted(in_recovery: bool) -> Self {
Self {
lag_ms: AtomicU64::new(LAG_UNMEASURABLE),
probed_at_unix_ms: AtomicU64::new(0),
in_recovery: AtomicBool::new(in_recovery),
booted_in_recovery: in_recovery,
}
}
fn is_still_a_standby(&self) -> bool {
!self.booted_in_recovery || self.in_recovery.load(Ordering::Relaxed)
}
fn within_lag_budget(&self, now_ms: u64, max_lag_ms: u64) -> bool {
let probed_at = self.probed_at_unix_ms.load(Ordering::Relaxed);
if probed_at == 0 {
return false;
}
let lag = self.lag_ms.load(Ordering::Relaxed);
if lag == LAG_UNMEASURABLE {
return false;
}
lag.saturating_add(now_ms.saturating_sub(probed_at)) <= max_lag_ms
}
fn record(&self, index: usize, in_recovery: bool, lag_ms: Option<u64>) {
if self.in_recovery.swap(in_recovery, Ordering::Relaxed) && !in_recovery {
tracing::warn!(
replica = index,
"Read replica left recovery (pg_is_in_recovery() = false); a failover has \
promoted it to a writable server. Reads routed to it may diverge from the \
primary, and its replication lag is no longer measurable."
);
}
self.lag_ms.store(lag_ms.unwrap_or(LAG_UNMEASURABLE), Ordering::Relaxed);
if lag_ms.is_some() {
self.probed_at_unix_ms.store(now_unix_ms(), Ordering::Relaxed);
}
}
fn record_failure(&self) {
self.lag_ms.store(LAG_UNMEASURABLE, Ordering::Relaxed);
}
}
async fn probe_read_replicas(set: Weak<ReadReplicaSet>) {
loop {
let Some(set) = set.upgrade() else { return };
let interval = set.probe_interval;
for (index, pool) in set.pools.iter().enumerate() {
let health = &set.health[index];
let client = match pool.get().await {
Ok(client) => client,
Err(e) => {
tracing::debug!(replica = index, error = %e, "Read replica health probe \
could not acquire a connection");
health.record_failure();
continue;
},
};
match client.query_one(REPLICA_PROBE_SQL, &[]).await {
Ok(row) => {
let in_recovery: bool = row.get(0);
let lag_ms: Option<i64> = row.get(1);
health.record(index, in_recovery, lag_ms.and_then(|ms| u64::try_from(ms).ok()));
},
Err(e) => {
tracing::debug!(
replica = index,
error = %pg_detail(&e),
"Read replica health probe failed"
);
health.record_failure();
},
}
}
drop(set);
tokio::time::sleep(interval).await;
}
}
const fn pin_active(now_ms: u64, last_write_ms: u64, pin_ms: u64) -> bool {
now_ms.saturating_sub(last_write_ms) < pin_ms
}
fn now_unix_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
}
impl std::fmt::Debug for PostgresAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PostgresAdapter")
.field("mutation_timing_enabled", &self.mutation_timing_enabled)
.field("timing_variable_name", &self.timing_variable_name)
.field("pool", &"<Pool>")
.field(
"read_replicas",
&self.read_replicas.as_ref().map(|r| r.pools.len()).unwrap_or_default(),
)
.field("streaming_permits", &self.streaming_permits.available_permits())
.finish()
}
}
impl PostgresAdapter {
pub async fn new(connection_string: &str) -> Result<Self> {
Self::with_pool_config(
connection_string,
PoolPrewarmConfig {
min_size: 0,
max_size: DEFAULT_POOL_SIZE,
timeout_secs: None,
search_path: None,
max_streaming_reads: None,
vector_scan: VectorScanConfig::default(),
tls: PostgresTlsConfig::default(),
read_replicas: None,
},
)
.await
}
pub async fn with_pool_config(connection_string: &str, cfg: PoolPrewarmConfig) -> Result<Self> {
let pool = build_pool(
connection_string,
cfg.max_size,
cfg.timeout_secs,
cfg.search_path.as_ref(),
cfg.vector_scan,
&cfg.tls,
)?;
let client = pool.get().await.map_err(|e| FraiseQLError::ConnectionPool {
message: format!("Failed to acquire connection: {e}"),
})?;
require_supported_server(&client).await?;
drop(client);
let read_replicas = match &cfg.read_replicas {
None => None,
Some(rc) => {
let set = Arc::new(build_read_replica_set(rc, &cfg).await?);
tokio::spawn(probe_read_replicas(Arc::downgrade(&set)));
Some(set)
},
};
let adapter = Self {
pool,
read_replicas,
mutation_timing_enabled: false,
timing_variable_name: "fraiseql.started_at".to_string(),
streaming_permits: Arc::new(Semaphore::new(streaming_read_slots(
cfg.max_streaming_reads,
cfg.max_size,
))),
};
let warm_target = cfg.min_size.min(cfg.max_size);
if warm_target > 0 {
adapter.prewarm(warm_target).await;
}
Ok(adapter)
}
pub async fn with_pool_size(connection_string: &str, max_size: usize) -> Result<Self> {
Self::with_pool_config(
connection_string,
PoolPrewarmConfig {
min_size: 0,
max_size,
timeout_secs: None,
search_path: None,
tls: PostgresTlsConfig::default(),
read_replicas: None,
max_streaming_reads: None,
vector_scan: VectorScanConfig::default(),
},
)
.await
}
async fn prewarm(&self, count: usize) {
use futures::future::join_all;
use tokio::time::timeout;
let result =
timeout(Duration::from_secs(10), join_all((0..count).map(|_| self.pool.get()))).await;
let (succeeded, failed) = match result {
Ok(guards) => {
let s = guards.iter().filter(|r| r.is_ok()).count();
drop(guards);
(s, count - s)
},
Err(_elapsed) => {
tracing::warn!(
target_connections = count,
"Pool pre-warm timed out after 10s; server will continue with partial pre-warm"
);
(0, count)
},
};
if failed > 0 {
tracing::warn!(
succeeded,
failed,
"Pool pre-warm: some connections could not be established"
);
} else {
tracing::info!(
idle_connections = self.pool.status().available,
"PostgreSQL pool pre-warmed successfully"
);
}
}
#[must_use]
pub const fn pool(&self) -> &Pool {
&self.pool
}
#[must_use]
pub fn with_mutation_timing(mut self, variable_name: &str) -> Self {
self.mutation_timing_enabled = true;
self.timing_variable_name = variable_name.to_string();
self
}
#[must_use]
pub const fn mutation_timing_enabled(&self) -> bool {
self.mutation_timing_enabled
}
pub(super) async fn execute_raw(
&self,
sql: &str,
params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
routing: ReadRouting,
) -> Result<Vec<JsonbValue>> {
let client = self.acquire_read_connection_with_retry(routing).await?;
let rows: Vec<Row> =
client.query(sql, params).await.map_err(|e| FraiseQLError::Database {
message: format!("Query execution failed: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
let results = rows
.into_iter()
.map(|row| jsonb_cell(&row, 0, sql))
.collect::<Result<Vec<_>>>()?;
Ok(results)
}
pub(super) async fn execute_raw_with_session(
&self,
sql: &str,
params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
session_vars: &[(&str, &str)],
routing: ReadRouting,
) -> Result<Vec<JsonbValue>> {
let mut client = self.acquire_read_connection_with_retry(routing).await?;
let txn =
client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to start session-var transaction: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
database::apply_session_vars(&txn, session_vars).await?;
let rows: Vec<Row> = txn.query(sql, params).await.map_err(|e| FraiseQLError::Database {
message: format!("Query execution failed: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
txn.commit().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to commit session-var transaction: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
rows.into_iter().map(|row| jsonb_cell(&row, 0, sql)).collect()
}
pub(super) fn mark_write(&self) {
if let Some(replicas) = &self.read_replicas {
replicas.last_write_unix_ms.store(now_unix_ms(), Ordering::Relaxed);
}
}
pub(super) async fn acquire_read_connection_with_retry(
&self,
routing: ReadRouting,
) -> Result<deadpool_postgres::Client> {
let Some(replicas) = &self.read_replicas else {
return self.acquire_connection_with_retry().await;
};
if !routing.allows_replica() {
return self.acquire_connection_with_retry().await;
}
let now_ms = now_unix_ms();
if routing.honours_write_pin()
&& pin_active(
now_ms,
replicas.last_write_unix_ms.load(Ordering::Relaxed),
replicas.pin_after_write_ms,
)
{
return self.acquire_connection_with_retry().await;
}
let max_lag_ms = routing.effective_max_lag_ms(replicas.max_lag_ms);
let count = replicas.pools.len();
let start = replicas.next.fetch_add(1, Ordering::Relaxed);
let mut ineligible = 0_usize;
for offset in 0..count {
let index = (start.wrapping_add(offset)) % count;
let health = &replicas.health[index];
let eligible = health.is_still_a_standby()
&& max_lag_ms.is_none_or(|budget| health.within_lag_budget(now_ms, budget));
if !eligible {
ineligible += 1;
continue;
}
match replicas.pools[index].get().await {
Ok(client) => return Ok(client),
Err(e) => {
tracing::warn!(
replica = index,
error = %e,
"Read replica connection acquisition failed; trying the next candidate"
);
},
}
}
if ineligible > 0 {
tracing::debug!(
ineligible,
replicas = count,
"Read replicas outside the bounded-staleness budget, or promoted out of \
recovery; serving the read from the primary"
);
} else {
tracing::warn!("All read replicas unavailable; serving the read from the primary");
}
self.acquire_connection_with_retry().await
}
pub(super) async fn acquire_connection_with_retry(&self) -> Result<deadpool_postgres::Client> {
use deadpool_postgres::PoolError;
let mut last_error = None;
for attempt in 0..MAX_CONNECTION_RETRIES {
match self.pool.get().await {
Ok(client) => {
if attempt > 0 {
tracing::info!(attempt, "Successfully acquired connection after retries");
}
return Ok(client);
},
Err(PoolError::Timeout(_)) => {
let metrics = self.pool_metrics();
tracing::error!(
available = metrics.idle_connections,
active = metrics.active_connections,
max = metrics.total_connections,
"Connection pool timeout: all connections busy"
);
return Err(FraiseQLError::ConnectionPool {
message: format!(
"Connection pool timeout: {}/{} connections busy. \
Increase pool_max_size or reduce concurrent load.",
metrics.active_connections, metrics.total_connections,
),
});
},
Err(e) => {
last_error = Some(e);
if attempt < MAX_CONNECTION_RETRIES - 1 {
let delay = CONNECTION_RETRY_DELAY_MS * (u64::from(attempt) + 1);
tracing::warn!(
attempt = attempt + 1,
total = MAX_CONNECTION_RETRIES,
delay_ms = delay,
"Transient connection error, retrying"
);
tokio::time::sleep(Duration::from_millis(delay)).await;
}
},
}
}
let pool_metrics = self.pool_metrics();
tracing::error!(
retries = MAX_CONNECTION_RETRIES,
available = pool_metrics.idle_connections,
active = pool_metrics.active_connections,
max = pool_metrics.total_connections,
"Failed to acquire connection after all retries"
);
Err(FraiseQLError::ConnectionPool {
message: format!(
"Failed to acquire connection after {} retries: {}. \
Pool state: idle={}, active={}, max={}",
MAX_CONNECTION_RETRIES,
last_error.expect("last_error is set on every retry iteration"),
pool_metrics.idle_connections,
pool_metrics.active_connections,
pool_metrics.total_connections,
),
})
}
pub(super) async fn execute_with_projection_impl(
&self,
view: &str,
projection: Option<&SqlProjectionHint>,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
) -> Result<Vec<JsonbValue>> {
if projection.is_none() {
return self.execute_where_query(view, where_clause, limit, offset, order_by).await;
}
let projection = projection.expect("projection is Some; None was returned above");
let (sql, typed_params) =
build_projection_select_sql(projection, view, where_clause, limit, offset, order_by)?;
tracing::debug!("SQL with projection = {}", sql);
tracing::debug!("typed_params = {:?}", typed_params);
let param_refs = crate::types::as_sql_param_refs(&typed_params);
self.execute_raw(&sql, ¶m_refs, ReadRouting::Any).await
}
pub async fn execute_with_projection(
&self,
view: &str,
projection: Option<&SqlProjectionHint>,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<Vec<JsonbValue>> {
self.execute_with_projection_impl(view, projection, where_clause, limit, offset, None)
.await
}
}
pub(super) fn build_where_select_sql(
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<(String, Vec<QueryParam>)> {
build_where_select_sql_ordered(view, where_clause, limit, offset, None)
}
pub(super) fn build_count_sql(
view: &str,
where_clause: Option<&WhereClause>,
) -> Result<(String, Vec<QueryParam>)> {
let mut sql = format!("SELECT COUNT(*) FROM {}", quote_postgres_identifier(view));
let typed_params: Vec<QueryParam> = if let Some(clause) = where_clause {
let generator = PostgresWhereGenerator::new(PostgresDialect);
let (where_sql, where_params) = generator.generate(clause)?;
sql.push_str(" WHERE ");
sql.push_str(&where_sql);
where_params.into_iter().map(QueryParam::from).collect()
} else {
Vec::new()
};
Ok((sql, typed_params))
}
pub(super) fn build_where_select_sql_ordered(
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
) -> Result<(String, Vec<QueryParam>)> {
let mut sql = format!("SELECT data FROM {}", quote_postgres_identifier(view));
let mut typed_params: Vec<QueryParam> = if let Some(clause) = where_clause {
let generator = PostgresWhereGenerator::new(PostgresDialect);
let (where_sql, where_params) = generator.generate(clause)?;
sql.push_str(" WHERE ");
sql.push_str(&where_sql);
where_params.into_iter().map(QueryParam::from).collect()
} else {
Vec::new()
};
let mut param_count = typed_params.len();
for bound in append_order_by(
&mut sql,
order_by,
DatabaseType::PostgreSQL,
param_count + 1,
Tiebreak::Identity,
)? {
param_count += 1;
typed_params.push(QueryParam::Text(bound));
}
if let Some(lim) = limit {
param_count += 1;
write!(sql, " LIMIT ${param_count}").expect("write to String");
typed_params.push(QueryParam::BigInt(i64::from(lim)));
}
if let Some(off) = offset {
param_count += 1;
write!(sql, " OFFSET ${param_count}").expect("write to String");
typed_params.push(QueryParam::BigInt(i64::from(off)));
}
Ok((sql, typed_params))
}
pub(super) fn build_projection_select_sql(
projection: &SqlProjectionHint,
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
) -> Result<(String, Vec<QueryParam>)> {
let mut sql = format!(
"SELECT {} FROM {}",
projection.projection_template,
quote_postgres_identifier(view)
);
let mut typed_params: Vec<QueryParam> = if let Some(clause) = where_clause {
let generator = PostgresWhereGenerator::new(PostgresDialect);
let (where_sql, where_params) = generator.generate(clause)?;
sql.push_str(" WHERE ");
sql.push_str(&where_sql);
where_params.into_iter().map(QueryParam::from).collect()
} else {
Vec::new()
};
let mut param_count = typed_params.len();
for bound in append_order_by(
&mut sql,
order_by,
DatabaseType::PostgreSQL,
param_count + 1,
Tiebreak::Identity,
)? {
param_count += 1;
typed_params.push(QueryParam::Text(bound));
}
if let Some(lim) = limit {
param_count += 1;
write!(sql, " LIMIT ${param_count}").expect("write to String");
typed_params.push(QueryParam::BigInt(i64::from(lim)));
}
if let Some(off) = offset {
param_count += 1;
write!(sql, " OFFSET ${param_count}").expect("write to String");
typed_params.push(QueryParam::BigInt(i64::from(off)));
}
Ok((sql, typed_params))
}