Files
openobserve/src/infra/src/db/postgres.rs
T
85d261b5d0 fix: vulnerability scan fixes (#11249)
| # | Severity | Category | File |
| :--- | :--- | :--- | :--- |
| 1 | **High** | SSRF bypass (IPv6) | `ssrf_guard.rs` |
| 2 | **High** | SSRF bypass (retry) | `enrichment_table/mod.rs` |
| 3 | **High** | SSRF defense-in-depth | `alert.rs` |
| 4 | **Critical** | CORS + credentials | `router/mod.rs` |
| 5 | **Critical** | SQL Injection | `postgres.rs`, `sqlite.rs` |
| 6 | **High** | Privilege escalation (token) | `users.rs` |
| 7 | **Medium** | Panic/DoS | `users.rs` |
| 8 | **Low** | Integer overflow | `alerts/mod.rs` |
| 9 | **Medium** | XSS | `alertSummaryGenerator.ts`, `colorizeQuery.ts`
|
| 10 | **Medium** | Credential exposure in logs | `validator.rs`,
`organization.rs`, `users.rs` |

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
Co-authored-by: Prabhat Sharma <hi.prabhat@gmail.com>
2026-04-14 07:22:11 +00:00

1349 lines
44 KiB
Rust

// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::{
collections::HashSet,
str::FromStr,
sync::{Arc, LazyLock as Lazy},
time::Duration,
};
use async_trait::async_trait;
use bytes::Bytes;
use config::{
metrics::{DB_QUERY_NUMS, DB_QUERY_TIME},
utils::{hash::Sum64, util::zero_or},
};
use hashbrown::HashMap;
use sqlx::{
Pool, Postgres,
postgres::{PgConnectOptions, PgPoolOptions},
};
use tokio::sync::{OnceCell, mpsc};
use super::{DBIndex, IndexStatement};
use crate::errors::*;
pub static CLIENT: Lazy<Pool<Postgres>> = Lazy::new(|| connect(false, false));
pub static CLIENT_RO: Lazy<Pool<Postgres>> = Lazy::new(|| connect(true, false));
pub static CLIENT_DDL: Lazy<Pool<Postgres>> = Lazy::new(|| connect(false, true));
static INDICES: OnceCell<HashSet<DBIndex>> = OnceCell::const_new();
fn connect(readonly: bool, ddl: bool) -> Pool<Postgres> {
let cfg = config::get_config();
let mut dsn = match (readonly, ddl) {
(true, false) => cfg.common.meta_postgres_ro_dsn.clone(),
(false, false) => cfg.common.meta_postgres_dsn.clone(),
(_, true) => cfg.common.meta_ddl_dsn.clone(),
};
if dsn.is_empty() {
// default fallback for any case is the original dsn, which is checked
// in config.rs to be non-empty
dsn = cfg.common.meta_postgres_dsn.clone();
}
let db_opts = PgConnectOptions::from_str(&dsn).expect("postgres connect options create failed");
let acquire_timeout = zero_or(cfg.limit.sql_db_connections_acquire_timeout, 30);
let idle_timeout = zero_or(cfg.limit.sql_db_connections_idle_timeout, 600);
let max_lifetime = zero_or(cfg.limit.sql_db_connections_max_lifetime, 1800);
PgPoolOptions::new()
.min_connections(cfg.limit.sql_db_connections_min)
.max_connections(cfg.limit.sql_db_connections_max)
.acquire_timeout(Duration::from_secs(acquire_timeout))
.idle_timeout(Some(Duration::from_secs(idle_timeout)))
.max_lifetime(Some(Duration::from_secs(max_lifetime)))
.connect_lazy_with(db_opts)
}
async fn cache_indices() -> HashSet<DBIndex> {
let client = CLIENT.clone();
DB_QUERY_NUMS
.with_label_values(&["select", "pg_indexes"])
.inc();
let sql = r#"SELECT indexname, tablename FROM pg_indexes;"#;
let res = sqlx::query_as::<_, (String, String)>(sql)
.fetch_all(&client)
.await;
match res {
Ok(r) => r
.into_iter()
.map(|(name, table)| DBIndex { name, table })
.collect(),
Err(_) => HashSet::new(),
}
}
pub async fn shutdown_ddl_pool() -> Result<()> {
CLIENT_DDL.close().await;
Ok(())
}
pub struct PostgresDb {}
impl PostgresDb {
pub fn new() -> Self {
Self {}
}
}
impl Default for PostgresDb {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl super::Db for PostgresDb {
async fn create_table(&self) -> Result<()> {
create_table().await
}
async fn stats(&self) -> Result<super::Stats> {
let pool = CLIENT_RO.clone();
DB_QUERY_NUMS.with_label_values(&["select", "meta"]).inc();
let keys_count: i64 = sqlx::query_scalar(r#"SELECT COUNT(*)::BIGINT AS num FROM meta;"#)
.fetch_one(&pool)
.await
.unwrap_or_default();
Ok(super::Stats {
bytes_len: 0,
keys_count,
})
}
async fn get(&self, key: &str) -> Result<Bytes> {
let (module, key1, key2) = super::parse_key(key);
let pool = CLIENT_RO.clone();
DB_QUERY_NUMS.with_label_values(&["select", "meta"]).inc();
let query = "SELECT value FROM meta WHERE module = $1 AND key1 = $2 AND key2 = $3 ORDER BY start_dt DESC;";
let value: String = match sqlx::query_scalar(query)
.bind(&module)
.bind(&key1)
.bind(&key2)
.fetch_one(&pool)
.await
{
Ok(v) => v,
Err(e) => {
if let sqlx::Error::RowNotFound = e {
return Err(Error::from(DbError::KeyNotExists(key.to_string())));
} else {
return Err(Error::from(DbError::DBOperError(
e.to_string(),
key.to_string(),
)));
}
}
};
Ok(Bytes::from(value))
}
async fn put(
&self,
key: &str,
value: Bytes,
need_watch: bool,
start_dt: Option<i64>,
) -> Result<()> {
let (module, key1, key2) = super::parse_key(key);
let pool = CLIENT.clone();
let local_start_dt = start_dt.unwrap_or_default();
let mut tx = pool.begin().await?;
DB_QUERY_NUMS.with_label_values(&["insert", "meta"]).inc();
let start = std::time::Instant::now();
if let Err(e) = sqlx::query(
r#"INSERT INTO meta (module, key1, key2, start_dt, value) VALUES ($1, $2, $3, $4, '') ON CONFLICT DO NOTHING;"#
)
.bind(&module)
.bind(&key1)
.bind(&key2)
.bind(local_start_dt)
.execute(&mut *tx)
.await
{
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback put meta error: {e}");
}
return Err(e.into());
}
// need commit it first to avoid the deadlock of insert and update
if let Err(e) = tx.commit().await {
log::error!("[POSTGRES] commit put meta error: {e}");
return Err(e.into());
}
DB_QUERY_NUMS.with_label_values(&["update", "meta"]).inc();
let mut tx = pool.begin().await?;
if let Err(e) = sqlx::query(
r#"UPDATE meta SET value = $1 WHERE module = $2 AND key1 = $3 AND key2 = $4 AND start_dt = $5;"#
)
.bind(String::from_utf8(value.to_vec()).unwrap_or_default())
.bind(&module)
.bind(&key1)
.bind(&key2)
.bind(local_start_dt)
.execute(&mut *tx)
.await
{
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback put meta error: {e}");
}
return Err(e.into());
}
if let Err(e) = tx.commit().await {
log::error!("[POSTGRES] commit put meta error: {e}");
return Err(e.into());
}
let time = start.elapsed().as_secs_f64();
// event watch
if need_watch {
let cluster_coordinator = super::get_coordinator().await;
cluster_coordinator
.put(key, Bytes::from(""), true, start_dt)
.await?;
}
DB_QUERY_TIME
.with_label_values(&["put_item", "meta"])
.observe(time);
Ok(())
}
async fn get_for_update(
&self,
key: &str,
need_watch: bool,
start_dt: Option<i64>,
update_fn: Box<super::UpdateFn>,
) -> Result<()> {
let (module, key1, key2) = super::parse_key(key);
let pool = CLIENT.clone();
let mut tx = pool.begin().await?;
let lock_key = format!("get_for_update_{key}");
let lock_id = config::utils::hash::gxhash::new().sum64(&lock_key);
let lock_id = if lock_id > i64::MAX as u64 {
(lock_id >> 1) as i64
} else {
lock_id as i64
};
DB_QUERY_NUMS.with_label_values(&["get_lock", ""]).inc();
if let Err(e) = sqlx::query("SELECT pg_advisory_xact_lock($1)")
.bind(lock_id)
.execute(&mut *tx)
.await
{
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback get_for_update error: {e}");
}
return Err(e.into());
};
let mut need_watch_dt = 0;
let row = if let Some(start_dt) = start_dt {
DB_QUERY_NUMS.with_label_values(&["select", "meta"]).inc();
match sqlx::query_as::<_,super::MetaRecord>(
r#"SELECT id, module, key1, key2, start_dt, value FROM meta WHERE module = $1 AND key1 = $2 AND key2 = $3 AND start_dt = $4;"#
)
.bind(&module)
.bind(&key1)
.bind(&key2)
.bind(start_dt)
.fetch_one(&mut *tx)
.await
{
Ok(v) => Some(v),
Err(e) => {
if e.to_string().contains("no rows returned") {
None
} else {
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback get_for_update error: {e}");
}
return Err(e.into());
}
}
}
} else {
DB_QUERY_NUMS.with_label_values(&["select", "meta"]).inc();
match sqlx::query_as::<_,super::MetaRecord>(
r#"SELECT id, module, key1, key2, start_dt, value FROM meta WHERE module = $1 AND key1 = $2 AND key2 = $3 ORDER BY start_dt DESC, id DESC;"#
)
.bind(&module)
.bind(&key1)
.bind(&key2)
.fetch_one(&mut *tx)
.await
{
Ok(v) => Some(v),
Err(e) => {
if e.to_string().contains("no rows returned") {
None
} else {
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback get_for_update error: {e}");
}
return Err(e.into());
}
}
}
};
let exist = row.is_some();
let row_id = row.as_ref().map(|r| r.id);
let value = row.map(|r| Bytes::from(r.value));
let (value, new_value) = match update_fn(value) {
Err(e) => {
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback get_for_update error: {e}");
}
return Err(e);
}
Ok(None) => {
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback get_for_update error: {e}");
}
return Ok(());
}
Ok(Some(v)) => v,
};
// update value
if let Some(value) = value {
let ret = if exist {
DB_QUERY_NUMS.with_label_values(&["update", "meta"]).inc();
sqlx::query(r#"UPDATE meta SET value = $1 WHERE id = $2;"#)
.bind(String::from_utf8(value.to_vec()).unwrap_or_default())
.bind(row_id.unwrap())
.execute(&mut *tx)
.await
} else {
DB_QUERY_NUMS.with_label_values(&["insert", "meta"]).inc();
sqlx::query(
r#"INSERT INTO meta (module, key1, key2, start_dt, value) VALUES ($1, $2, $3, $4, $5);"#
)
.bind(&module)
.bind(&key1)
.bind(&key2)
.bind(start_dt.unwrap_or_default())
.bind(String::from_utf8(value.to_vec()).unwrap_or_default())
.execute(&mut *tx)
.await
};
if let Err(e) = ret {
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback get_for_update error: {e}");
}
return Err(e.into());
}
}
// new value
if let Some((new_key, new_value, new_start_dt)) = new_value {
need_watch_dt = new_start_dt.unwrap_or_default();
let (module, key1, key2) = super::parse_key(&new_key);
DB_QUERY_NUMS.with_label_values(&["insert", "meta"]).inc();
if let Err(e) = sqlx::query(
r#"INSERT INTO meta (module, key1, key2, start_dt, value) VALUES ($1, $2, $3, $4, $5);"#
)
.bind(&module)
.bind(&key1)
.bind(&key2)
.bind(new_start_dt.unwrap_or_default())
.bind(String::from_utf8(new_value.to_vec()).unwrap_or_default())
.execute(&mut *tx)
.await
{
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback get_for_update error: {e}");
}
return Err(e.into());
}
}
if let Err(e) = tx.commit().await {
log::error!("[POSTGRES] commit get_for_update error: {e}");
return Err(e.into());
}
// event watch
if need_watch {
let start_dt = if need_watch_dt > 0 {
Some(need_watch_dt)
} else {
start_dt
};
let cluster_coordinator = super::get_coordinator().await;
cluster_coordinator
.put(key, Bytes::from(""), true, start_dt)
.await?;
}
Ok(())
}
async fn delete(
&self,
key: &str,
with_prefix: bool,
need_watch: bool,
start_dt: Option<i64>,
) -> Result<()> {
// event watch
if need_watch {
// find all keys then send event
let items = if with_prefix {
self.list_keys(key).await?
} else {
vec![key.to_string()]
};
let cluster_coordinator = super::get_coordinator().await;
tokio::task::spawn(async move {
for key in items {
if let Err(e) = cluster_coordinator
.delete(&key, false, true, start_dt)
.await
{
log::error!("[POSTGRES] send event error: {e}");
}
}
});
}
let (module, key1, key2) = super::parse_key(key);
let pool = CLIENT.clone();
DB_QUERY_NUMS.with_label_values(&["delete", "meta"]).inc();
if with_prefix {
if key1.is_empty() {
// module = $1 [, start_dt = $2]
if let Some(dt) = start_dt {
sqlx::query("DELETE FROM meta WHERE module = $1 AND start_dt = $2")
.bind(&module)
.bind(dt)
.execute(&pool)
.await?;
} else {
sqlx::query("DELETE FROM meta WHERE module = $1")
.bind(&module)
.execute(&pool)
.await?;
}
} else if key2.is_empty() {
// module = $1, key1 = $2 [, start_dt = $3]
if let Some(dt) = start_dt {
sqlx::query(
"DELETE FROM meta WHERE module = $1 AND key1 = $2 AND start_dt = $3",
)
.bind(&module)
.bind(&key1)
.bind(dt)
.execute(&pool)
.await?;
} else {
sqlx::query("DELETE FROM meta WHERE module = $1 AND key1 = $2")
.bind(&module)
.bind(&key1)
.execute(&pool)
.await?;
}
} else {
// module = $1, key1 = $2, key2 = $3, key2/% = $4 [, start_dt = $5]
if let Some(dt) = start_dt {
sqlx::query("DELETE FROM meta WHERE module = $1 AND key1 = $2 AND (key2 = $3 OR key2 LIKE $4) AND start_dt = $5")
.bind(&module)
.bind(&key1)
.bind(&key2)
.bind(format!("{}/%", key2))
.bind(dt)
.execute(&pool)
.await?;
} else {
sqlx::query("DELETE FROM meta WHERE module = $1 AND key1 = $2 AND (key2 = $3 OR key2 LIKE $4)")
.bind(&module)
.bind(&key1)
.bind(&key2)
.bind(format!("{}/%", key2))
.execute(&pool)
.await?;
}
}
} else {
// module = $1, key1 = $2, key2 = $3 [, start_dt = $4]
if let Some(dt) = start_dt {
sqlx::query("DELETE FROM meta WHERE module = $1 AND key1 = $2 AND key2 = $3 AND start_dt = $4")
.bind(&module)
.bind(&key1)
.bind(&key2)
.bind(dt)
.execute(&pool)
.await?;
} else {
sqlx::query("DELETE FROM meta WHERE module = $1 AND key1 = $2 AND key2 = $3")
.bind(&module)
.bind(&key1)
.bind(&key2)
.execute(&pool)
.await?;
}
}
Ok(())
}
async fn list(&self, prefix: &str) -> Result<HashMap<String, Bytes>> {
let (module, key1, key2) = super::parse_key(prefix);
let mut sql =
"SELECT id, module, key1, key2, start_dt, value FROM meta WHERE 1=1".to_string();
let mut params = Vec::new();
if !module.is_empty() {
sql.push_str(" AND module = $1");
params.push(module);
}
if !key1.is_empty() {
sql.push_str(&format!(" AND key1 = ${}", params.len() + 1));
params.push(key1);
}
if !key2.is_empty() {
sql.push_str(&format!(
" AND (key2 = ${} OR key2 LIKE ${})",
params.len() + 1,
params.len() + 2
));
let key2_prefix = format!("{}/%", key2);
params.push(key2);
params.push(key2_prefix);
}
sql.push_str(" ORDER BY start_dt ASC");
let pool = CLIENT_RO.clone();
DB_QUERY_NUMS.with_label_values(&["select", "meta"]).inc();
let mut query = sqlx::query_as::<_, super::MetaRecord>(&sql);
for p in params {
query = query.bind(p);
}
let ret = query.fetch_all(&pool).await?;
Ok(ret
.into_iter()
.map(|r| {
(
super::build_key(&r.module, &r.key1, &r.key2, r.start_dt),
Bytes::from(r.value),
)
})
.collect())
}
async fn list_keys(&self, prefix: &str) -> Result<Vec<String>> {
let (module, key1, key2) = super::parse_key(prefix);
let mut sql =
"SELECT id, module, key1, key2, start_dt, '' AS value FROM meta WHERE 1=1 ".to_string();
let mut params = Vec::new();
if !module.is_empty() {
sql.push_str(" AND module = $1");
params.push(module);
}
if !key1.is_empty() {
sql.push_str(&format!(" AND key1 = ${}", params.len() + 1));
params.push(key1);
}
if !key2.is_empty() {
sql.push_str(&format!(
" AND (key2 = ${} OR key2 LIKE ${})",
params.len() + 1,
params.len() + 2
));
let key2_prefix = format!("{}/%", key2);
params.push(key2);
params.push(key2_prefix);
}
sql.push_str(" ORDER BY start_dt ASC");
let pool = CLIENT_RO.clone();
DB_QUERY_NUMS.with_label_values(&["select", "meta"]).inc();
let mut query = sqlx::query_as::<_, super::MetaRecord>(&sql);
for p in params {
query = query.bind(p);
}
let ret = query.fetch_all(&pool).await?;
Ok(ret
.into_iter()
.map(|r| format!("/{}/{}/{}", r.module, r.key1, r.key2))
.collect())
}
async fn list_values(&self, prefix: &str) -> Result<Vec<Bytes>> {
let mut items = self.list(prefix).await?;
let mut keys = items.keys().map(|k| k.to_string()).collect::<Vec<_>>();
keys.sort();
Ok(keys
.into_iter()
.map(|k| items.remove(&k).unwrap())
.collect())
}
async fn list_values_by_start_dt(
&self,
prefix: &str,
start_dt: Option<(i64, i64)>,
) -> Result<Vec<(i64, Bytes)>> {
if start_dt.is_none() || start_dt == Some((0, 0)) {
let vals = self.list_values(prefix).await?;
return Ok(vals.into_iter().map(|v| (0, v)).collect());
}
let (min_dt, max_dt) = start_dt.unwrap();
let (sql, str_params) = build_list_by_start_dt_sql(prefix, min_dt, max_dt);
let pool = CLIENT_RO.clone();
DB_QUERY_NUMS.with_label_values(&["select", "meta"]).inc();
let mut query = sqlx::query_as::<_, super::MetaRecord>(&sql);
for p in str_params {
query = query.bind(p);
}
// min_dt and max_dt are i64 — bind them directly so PostgreSQL receives
// them as BIGINT rather than TEXT, avoiding "operator does not exist: bigint >= text".
query = query.bind(min_dt);
query = query.bind(max_dt);
let ret = query.fetch_all(&pool).await?;
Ok(ret
.into_iter()
.map(|r| (r.start_dt, Bytes::from(r.value)))
.collect())
}
async fn count(&self, prefix: &str) -> Result<i64> {
let (module, key1, key2) = super::parse_key(prefix);
let mut sql = "SELECT COUNT(*) AS num FROM meta WHERE 1=1".to_string();
let mut params = Vec::new();
if !module.is_empty() {
sql.push_str(" AND module = $1");
params.push(module);
}
if !key1.is_empty() {
sql.push_str(&format!(" AND key1 = ${}", params.len() + 1));
params.push(key1);
}
if !key2.is_empty() {
sql.push_str(&format!(
" AND (key2 = ${} OR key2 LIKE ${})",
params.len() + 1,
params.len() + 2
));
let key2_prefix = format!("{}/%", key2);
params.push(key2);
params.push(key2_prefix);
}
let pool = CLIENT_RO.clone();
DB_QUERY_NUMS.with_label_values(&["select", "meta"]).inc();
let mut query = sqlx::query_scalar::<_, i64>(&sql);
for p in params {
query = query.bind(p);
}
let count: i64 = query.fetch_one(&pool).await?;
Ok(count)
}
async fn watch(&self, _prefix: &str) -> Result<Arc<mpsc::Receiver<super::Event>>> {
Err(Error::NotImplemented)
}
async fn close(&self) -> Result<()> {
Ok(())
}
async fn add_start_dt_column(&self) -> Result<()> {
create_meta_backup().await?;
add_start_dt_column().await?;
Ok(())
}
}
pub async fn create_table() -> Result<()> {
let pool = CLIENT_DDL.clone();
DB_QUERY_NUMS.with_label_values(&["create", "meta"]).inc();
// create table
_ = sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS meta
(
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
module VARCHAR(100) not null,
key1 VARCHAR(256) not null,
key2 VARCHAR(256) not null,
start_dt BIGINT not null,
value TEXT not null
);
"#,
)
.execute(&pool)
.await?;
// create start_dt column for old version <= 0.9.2
DB_QUERY_NUMS
.with_label_values(&["select", "information_schema.columns"])
.inc();
let has_start_dt = sqlx::query_scalar::<_,i64>("SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='meta' AND column_name='start_dt';")
.fetch_one(&pool)
.await?;
if has_start_dt == 0 {
add_start_dt_column().await?;
}
// create table index
create_index(IndexStatement::new(
"meta_module_idx",
"meta",
false,
&["module"],
))
.await?;
create_index(IndexStatement::new(
"meta_module_key1_idx",
"meta",
false,
&["module", "key1"],
))
.await?;
create_index(IndexStatement::new(
"meta_module_start_dt_idx",
"meta",
true,
&["module", "key1", "key2", "start_dt"],
))
.await?;
Ok(())
}
async fn add_start_dt_column() -> Result<()> {
DB_QUERY_NUMS.with_label_values(&["alter", "meta"]).inc();
add_column("meta", "start_dt", "BIGINT NOT NULL DEFAULT 0").await?;
// Proceed to drop the index if it exists and create a new one if it does not exist
create_index(IndexStatement::new(
"meta_module_start_dt_idx",
"meta",
true,
&["module", "key1", "key2", "start_dt"],
))
.await?;
delete_index("meta_module_key2_idx", "meta").await?;
Ok(())
}
async fn create_meta_backup() -> Result<()> {
let pool = CLIENT_DDL.clone();
let mut tx = pool.begin().await?;
DB_QUERY_NUMS
.with_label_values(&["create", "meta_backup_20240330"])
.inc();
if let Err(e) =
sqlx::query(r#"CREATE TABLE IF NOT EXISTS meta_backup_20240330 AS SELECT * FROM meta;"#)
.execute(&mut *tx)
.await
{
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] rollback create table meta_backup_20240330 error: {e}");
}
return Err(e.into());
}
if let Err(e) = tx.commit().await {
log::error!("[POSTGRES] commit create table meta_backup_20240330 error: {e}");
return Err(e.into());
}
Ok(())
}
pub async fn create_index(index: IndexStatement<'_>) -> Result<()> {
let client = CLIENT_DDL.clone();
let indices = INDICES.get_or_init(cache_indices).await;
if indices.contains(&DBIndex {
name: index.idx_name.into(),
table: index.table.into(),
}) {
return Ok(());
}
let unique_str = if index.unique { "UNIQUE" } else { "" };
log::info!(
"[POSTGRES] creating index {} on table {}",
index.idx_name,
index.table
);
DB_QUERY_NUMS
.with_label_values(&["create", index.table])
.inc();
let sql = format!(
"CREATE {} INDEX IF NOT EXISTS {} ON {} ({});",
unique_str,
index.idx_name,
index.table,
index.fields.join(",")
);
let start = std::time::Instant::now();
sqlx::query(&sql).execute(&client).await?;
let time = start.elapsed().as_secs_f64();
DB_QUERY_TIME
.with_label_values(&["create_index", index.table])
.observe(time);
log::info!("[POSTGRES] index {} created successfully", index.idx_name);
Ok(())
}
pub async fn delete_index(idx_name: &str, table: &str) -> Result<()> {
let client = CLIENT_DDL.clone();
let indices = INDICES.get_or_init(cache_indices).await;
if !indices.contains(&DBIndex {
name: idx_name.into(),
table: table.into(),
}) {
return Ok(());
}
log::info!("[POSTGRES] deleting index {idx_name} on table {table}");
let sql = format!("DROP INDEX IF EXISTS {idx_name};");
DB_QUERY_NUMS.with_label_values(&["drop", table]).inc();
let start = std::time::Instant::now();
sqlx::query(&sql).execute(&client).await?;
let time = start.elapsed().as_secs_f64();
DB_QUERY_TIME
.with_label_values(&["drop_index", table])
.observe(time);
log::info!("[POSTGRES] index {idx_name} deleted successfully");
Ok(())
}
pub async fn add_column(table: &str, column: &str, data_type: &str) -> Result<()> {
let pool = CLIENT_DDL.clone();
let check_sql = format!(
"SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='{table}' AND column_name='{column}';"
);
let has_column = sqlx::query_scalar::<_, i64>(&check_sql)
.fetch_one(&pool)
.await?;
if has_column > 0 {
return Ok(());
}
let alert_sql = format!("ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {column} {data_type};");
let mut tx = pool.begin().await?;
if let Err(e) = sqlx::query(&alert_sql).execute(&mut *tx).await {
log::error!("[POSTGRES] Error in adding column {column}: {e}");
if let Err(e) = tx.rollback().await {
log::error!("[POSTGRES] Error in rolling back transaction: {e}");
}
return Err(e.into());
}
if let Err(e) = tx.commit().await {
log::info!("[POSTGRES] Error in committing transaction: {e}");
return Err(e.into());
}
Ok(())
}
pub async fn drop_column(table: &str, column: &str) -> Result<()> {
let pool = CLIENT_DDL.clone();
let check_sql = format!(
"SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='{table}' AND column_name='{column}';"
);
let has_column = sqlx::query_scalar::<_, i64>(&check_sql)
.fetch_one(&pool)
.await?;
if has_column > 0 {
let alert_sql = format!("ALTER TABLE {table} DROP COLUMN {column};");
sqlx::query(&alert_sql).execute(&pool).await?;
}
Ok(())
}
/// Builds the SQL string and string-typed bind parameters for `list_values_by_start_dt`.
///
/// Returns `(sql, str_params)` where `str_params` contains the string-typed positional
/// parameters (module / key1 / key2). The caller must then bind `min_dt` and `max_dt`
/// as `i64` **after** the string params so that PostgreSQL sees them as BIGINT, not TEXT.
///
/// Keeping this logic in a standalone function makes the SQL generation unit-testable
/// without requiring a live database connection.
pub(crate) fn build_list_by_start_dt_sql(
prefix: &str,
min_dt: i64,
max_dt: i64,
) -> (String, Vec<String>) {
let (module, key1, key2) = super::parse_key(prefix);
let mut sql = "SELECT id, module, key1, key2, start_dt, value FROM meta WHERE 1=1".to_string();
let mut params: Vec<String> = Vec::new();
if !module.is_empty() {
sql.push_str(" AND module = $1");
params.push(module);
}
if !key1.is_empty() {
sql.push_str(&format!(" AND key1 = ${}", params.len() + 1));
params.push(key1);
}
if !key2.is_empty() {
sql.push_str(&format!(
" AND (key2 = ${} OR key2 LIKE ${})",
params.len() + 1,
params.len() + 2
));
let key2_prefix = format!("{key2}/%");
params.push(key2);
params.push(key2_prefix);
}
// min_dt / max_dt are intentionally NOT pushed into params here.
// The caller binds them as i64 after the string-params loop so that the
// PostgreSQL driver receives them as BIGINT, not TEXT.
let n = params.len();
sql.push_str(&format!(
" AND start_dt >= ${} AND start_dt <= ${}",
n + 1,
n + 2
));
// Suppress unused-variable warning: min_dt / max_dt are consumed by the caller.
let _ = (min_dt, max_dt);
sql.push_str(" ORDER BY start_dt ASC");
(sql, params)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_postgres_db_new() {
let db = PostgresDb::new();
assert_eq!(std::mem::size_of_val(&db), 0);
}
#[test]
fn test_postgres_db_default() {
let db = PostgresDb::default();
assert_eq!(std::mem::size_of_val(&db), 0);
}
#[test]
fn test_parse_key_full() {
let key = "/module/key1/key2";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "module");
assert_eq!(k1, "key1");
assert_eq!(k2, "key2");
}
#[test]
fn test_parse_key_no_leading_slash() {
let key = "module/key1/key2";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "module");
assert_eq!(k1, "key1");
assert_eq!(k2, "key2");
}
#[test]
fn test_parse_key_module_only() {
let key = "/module";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "module");
assert_eq!(k1, "");
assert_eq!(k2, "");
}
#[test]
fn test_parse_key_module_and_key1() {
let key = "/module/key1";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "module");
assert_eq!(k1, "key1");
assert_eq!(k2, "");
}
#[test]
fn test_parse_key_with_slashes_in_key2() {
let key = "/module/key1/key2/subkey/subsubkey";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "module");
assert_eq!(k1, "key1");
assert_eq!(k2, "key2/subkey/subsubkey");
}
#[test]
fn test_parse_key_empty() {
let key = "";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "");
assert_eq!(k1, "");
assert_eq!(k2, "");
}
#[test]
fn test_build_key_without_start_dt() {
let key = super::super::build_key("module", "key1", "key2", 0);
assert_eq!(key, "/module/key1/key2");
}
#[test]
fn test_build_key_with_start_dt() {
let key = super::super::build_key("module", "key1", "key2", 1234567890);
assert_eq!(key, "/module/key1/key2/1234567890");
}
#[test]
fn test_build_key_empty_parts() {
let key = super::super::build_key("", "", "", 0);
assert_eq!(key, "//");
}
#[test]
fn test_build_key_with_special_chars() {
let key = super::super::build_key("mod-ule", "key_1", "key.2", 0);
assert_eq!(key, "/mod-ule/key_1/key.2");
}
#[test]
fn test_index_statement_new() {
let stmt = IndexStatement::new("test_idx", "test_table", false, &["col1", "col2"]);
assert_eq!(stmt.idx_name, "test_idx");
assert_eq!(stmt.table, "test_table");
assert!(!stmt.unique);
assert_eq!(stmt.fields, vec!["col1", "col2"]);
}
#[test]
fn test_index_statement_unique() {
let stmt = IndexStatement::new("unique_idx", "table", true, &["id"]);
assert_eq!(stmt.idx_name, "unique_idx");
assert_eq!(stmt.table, "table");
assert!(stmt.unique);
assert_eq!(stmt.fields, vec!["id"]);
}
#[test]
fn test_db_index_hash_same() {
use std::collections::HashSet;
let idx1 = DBIndex {
name: "test_idx".to_string(),
table: "test_table".to_string(),
};
let idx2 = DBIndex {
name: "test_idx".to_string(),
table: "test_table".to_string(),
};
let mut set = HashSet::new();
set.insert(idx1);
assert!(set.contains(&idx2));
}
#[test]
fn test_db_index_hash_different_name() {
use std::collections::HashSet;
let idx1 = DBIndex {
name: "idx1".to_string(),
table: "table".to_string(),
};
let idx2 = DBIndex {
name: "idx2".to_string(),
table: "table".to_string(),
};
let mut set = HashSet::new();
set.insert(idx1);
assert!(!set.contains(&idx2));
}
#[test]
fn test_add_column_check_sql_format() {
let table = "test_table";
let column = "new_column";
let check_sql = format!(
"SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='{table}' AND column_name='{column}';"
);
assert_eq!(
check_sql,
"SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='test_table' AND column_name='new_column';"
);
}
#[test]
fn test_add_column_alter_sql_format() {
let table = "test_table";
let column = "new_column";
let data_type = "VARCHAR(255) NOT NULL DEFAULT ''";
// PostgreSQL supports IF NOT EXISTS
let alert_sql =
format!("ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {column} {data_type};");
assert_eq!(
alert_sql,
"ALTER TABLE test_table ADD COLUMN IF NOT EXISTS new_column VARCHAR(255) NOT NULL DEFAULT '';"
);
}
#[test]
fn test_drop_column_check_sql_format() {
let table = "test_table";
let column = "old_column";
let check_sql = format!(
"SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='{table}' AND column_name='{column}';"
);
assert_eq!(
check_sql,
"SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='test_table' AND column_name='old_column';"
);
}
#[test]
fn test_drop_column_alter_sql_format() {
let table = "test_table";
let column = "old_column";
let alert_sql = format!("ALTER TABLE {table} DROP COLUMN {column};");
assert_eq!(alert_sql, "ALTER TABLE test_table DROP COLUMN old_column;");
}
#[test]
fn test_add_column_sql_with_various_postgres_types() {
let table = "users";
// BIGINT type
let alert_sql = format!(
"ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} {};",
table, "big_id", "BIGINT NOT NULL DEFAULT 0"
);
assert_eq!(
alert_sql,
"ALTER TABLE users ADD COLUMN IF NOT EXISTS big_id BIGINT NOT NULL DEFAULT 0;"
);
// VARCHAR type
let alert_sql = format!(
"ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} {};",
table, "email", "VARCHAR(255)"
);
assert_eq!(
alert_sql,
"ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255);"
);
// TEXT type
let alert_sql = format!(
"ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} {};",
table, "description", "TEXT"
);
assert_eq!(
alert_sql,
"ALTER TABLE users ADD COLUMN IF NOT EXISTS description TEXT;"
);
// TIMESTAMP type
let alert_sql = format!(
"ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} {};",
table, "created_at", "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP"
);
assert_eq!(
alert_sql,
"ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;"
);
// BOOLEAN type
let alert_sql = format!(
"ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} {};",
table, "is_active", "BOOLEAN NOT NULL DEFAULT TRUE"
);
assert_eq!(
alert_sql,
"ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT TRUE;"
);
// JSONB type (PostgreSQL specific)
let alert_sql = format!(
"ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} {};",
table, "metadata", "JSONB"
);
assert_eq!(
alert_sql,
"ALTER TABLE users ADD COLUMN IF NOT EXISTS metadata JSONB;"
);
// UUID type (PostgreSQL specific)
let alert_sql = format!(
"ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} {};",
table, "uuid", "UUID"
);
assert_eq!(
alert_sql,
"ALTER TABLE users ADD COLUMN IF NOT EXISTS uuid UUID;"
);
}
#[test]
fn test_sql_quote_escaping() {
let key = "key'with'quotes";
let escaped = key.replace('\'', "''");
assert_eq!(escaped, "key''with''quotes");
}
#[test]
fn test_sql_like_pattern_construction() {
let key2 = "prefix";
let pattern = format!("{key2}/%");
assert_eq!(pattern, "prefix/%");
}
#[test]
fn test_build_key_roundtrip() {
let original_parts = ("module", "key1", "key2");
let key = super::super::build_key(original_parts.0, original_parts.1, original_parts.2, 0);
let (module, k1, k2) = super::super::parse_key(&key);
assert_eq!(module, original_parts.0);
assert_eq!(k1, original_parts.1);
assert_eq!(k2, original_parts.2);
}
#[test]
fn test_parse_key_with_numbers() {
let key = "/123/456/789";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "123");
assert_eq!(k1, "456");
assert_eq!(k2, "789");
}
#[test]
fn test_parse_key_with_hyphens_underscores() {
let key = "/test-module/test_key1/test-key-2";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "test-module");
assert_eq!(k1, "test_key1");
assert_eq!(k2, "test-key-2");
}
#[test]
fn test_index_statement_empty_fields() {
let stmt = IndexStatement::new("empty_idx", "table", false, &[]);
assert_eq!(stmt.fields.len(), 0);
}
#[test]
fn test_parse_key_trailing_slash() {
let key = "/module/key1/key2/";
let (module, k1, k2) = super::super::parse_key(key);
assert_eq!(module, "module");
assert_eq!(k1, "key1");
assert!(k2.starts_with("key2"));
}
// ── build_list_by_start_dt_sql unit tests ─────────────────────────────────
// These tests verify that the SQL and parameter vector produced by the helper
// are correct for every combination of module / key1 / key2 presence, and that
// min_dt / max_dt placeholder indices are always one past the last string param.
#[test]
fn test_build_list_by_start_dt_sql_full_prefix() {
let (sql, params) = build_list_by_start_dt_sql("/module/key1/key2", 100, 200);
// 3 string params: module=$1, key1=$2, key2=$3 / key2-prefix=$4
assert_eq!(params.len(), 4);
assert_eq!(params[0], "module");
assert_eq!(params[1], "key1");
assert_eq!(params[2], "key2");
assert_eq!(params[3], "key2/%");
assert!(
sql.contains("AND module = $1"),
"module param missing: {sql}"
);
assert!(sql.contains("AND key1 = $2"), "key1 param missing: {sql}");
assert!(
sql.contains("AND (key2 = $3 OR key2 LIKE $4)"),
"key2 param missing: {sql}"
);
// min_dt=$5, max_dt=$6 (one past the 4 string params)
assert!(
sql.contains("AND start_dt >= $5 AND start_dt <= $6"),
"start_dt bounds wrong: {sql}"
);
assert!(
sql.ends_with("ORDER BY start_dt ASC"),
"missing ORDER BY: {sql}"
);
}
#[test]
fn test_build_list_by_start_dt_sql_module_only() {
let (sql, params) = build_list_by_start_dt_sql("/module", 1, 999);
assert_eq!(params.len(), 1);
assert_eq!(params[0], "module");
assert!(sql.contains("AND module = $1"));
// min_dt=$2, max_dt=$3
assert!(
sql.contains("AND start_dt >= $2 AND start_dt <= $3"),
"start_dt bounds wrong: {sql}"
);
}
#[test]
fn test_build_list_by_start_dt_sql_module_and_key1() {
let (sql, params) = build_list_by_start_dt_sql("/module/key1", 10, 20);
assert_eq!(params.len(), 2);
assert_eq!(params[0], "module");
assert_eq!(params[1], "key1");
assert!(sql.contains("AND module = $1"));
assert!(sql.contains("AND key1 = $2"));
// min_dt=$3, max_dt=$4
assert!(
sql.contains("AND start_dt >= $3 AND start_dt <= $4"),
"start_dt bounds wrong: {sql}"
);
}
#[test]
fn test_build_list_by_start_dt_sql_empty_prefix() {
let (sql, params) = build_list_by_start_dt_sql("", 0, 9999);
// No string params — min_dt=$1, max_dt=$2
assert!(params.is_empty());
assert!(
sql.contains("AND start_dt >= $1 AND start_dt <= $2"),
"start_dt bounds wrong for empty prefix: {sql}"
);
assert!(sql.contains("WHERE 1=1"));
}
#[test]
fn test_build_list_by_start_dt_sql_key2_prefix_pattern() {
let (sql, params) = build_list_by_start_dt_sql("/m/k1/k2", 0, 0);
// key2 LIKE pattern must end with /%
assert_eq!(params[3], "k2/%");
assert!(sql.contains("OR key2 LIKE $4"));
}
#[test]
fn test_build_list_by_start_dt_sql_starts_with_base_select() {
let (sql, _) = build_list_by_start_dt_sql("/m/k1/k2", 1, 2);
assert!(
sql.starts_with("SELECT id, module, key1, key2, start_dt, value FROM meta WHERE 1=1"),
"unexpected SQL start: {sql}"
);
}
}