#[cfg(all(feature = "sqlite", feature = "postgres"))]
compile_error!("features `sqlite` and `postgres` are mutually exclusive for `zeph-db`");
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
compile_error!("exactly one of `sqlite` or `postgres` must be enabled for `zeph-db`");
pub mod bounds;
pub mod dialect;
pub mod driver;
pub mod error;
pub mod fts;
pub mod graph_store;
pub mod instance_id;
pub mod migrate;
pub mod pool;
pub mod transaction;
pub use bounds::FullDriver;
pub use dialect::{Dialect, Postgres, Sqlite};
pub use driver::DatabaseDriver;
pub use error::DbError;
pub use graph_store::{GraphSummary, RawGraphStore};
pub use instance_id::get_or_create_instance_id;
pub use migrate::run_migrations;
pub use pool::{DbConfig, redact_url};
pub use transaction::{begin, begin_write};
pub use sqlx::query_builder::QueryBuilder;
pub use sqlx::{Error as SqlxError, Executor, FromRow, Row, query, query_as, query_scalar};
pub use sqlx;
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
pub type ActiveDriver = driver::SqliteDriver;
#[cfg(feature = "postgres")]
pub type ActiveDriver = driver::PostgresDriver;
pub type DbPool = sqlx::Pool<<ActiveDriver as DatabaseDriver>::Database>;
pub type DbRow = <<ActiveDriver as DatabaseDriver>::Database as sqlx::Database>::Row;
pub type DbQueryResult =
<<ActiveDriver as DatabaseDriver>::Database as sqlx::Database>::QueryResult;
pub type DbTransaction<'a> = sqlx::Transaction<'a, <ActiveDriver as DatabaseDriver>::Database>;
pub type ActiveDialect = <ActiveDriver as DatabaseDriver>::Dialect;
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
#[macro_export]
macro_rules! sql {
($query:expr) => {
$query
};
}
#[cfg(feature = "postgres")]
#[macro_export]
macro_rules! sql {
($query:expr) => {{
static CACHE: ::std::sync::LazyLock<::std::string::String> =
::std::sync::LazyLock::new(|| $crate::rewrite_placeholders($query));
CACHE.as_str()
}};
}
#[must_use]
pub fn is_postgres_url(url: &str) -> bool {
url.starts_with("postgres://") || url.starts_with("postgresql://")
}
#[must_use]
pub fn rewrite_placeholders(query: &str) -> String {
let mut out = String::with_capacity(query.len() + 16);
let mut chars = query.chars().peekable();
let mut max_n = 0u32;
let mut in_string = false;
while let Some(ch) = chars.next() {
match ch {
'\'' => {
in_string = !in_string;
out.push(ch);
}
'?' if !in_string => {
let digits: String =
std::iter::from_fn(|| chars.next_if(char::is_ascii_digit)).collect();
let n = if digits.is_empty() {
max_n += 1;
max_n
} else {
let explicit = digits.parse().unwrap_or(u32::MAX);
max_n = max_n.max(explicit);
explicit
};
out.push('$');
out.push_str(&n.to_string());
}
_ => out.push(ch),
}
}
out
}
#[must_use]
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
pub fn numbered_placeholder(n: usize) -> String {
format!("?{n}")
}
#[must_use]
#[cfg(feature = "postgres")]
pub fn numbered_placeholder(n: usize) -> String {
format!("${n}")
}
#[must_use]
pub fn placeholder_list(start: usize, count: usize) -> String {
(start..start + count)
.map(numbered_placeholder)
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rewrite_placeholders_basic() {
let out = rewrite_placeholders("SELECT * FROM t WHERE a = ? AND b = ?");
assert_eq!(out, "SELECT * FROM t WHERE a = $1 AND b = $2");
}
#[test]
fn rewrite_placeholders_skips_string_literals() {
let out = rewrite_placeholders("SELECT '?' FROM t WHERE a = ?");
assert_eq!(out, "SELECT '?' FROM t WHERE a = $1");
}
#[test]
fn rewrite_placeholders_no_params() {
let out = rewrite_placeholders("SELECT 1");
assert_eq!(out, "SELECT 1");
}
#[test]
fn rewrite_placeholders_numbered() {
let out = rewrite_placeholders("INSERT INTO t (a, b) VALUES (?1, ?2) RETURNING id");
assert_eq!(out, "INSERT INTO t (a, b) VALUES ($1, $2) RETURNING id");
}
#[test]
fn rewrite_placeholders_collapses_repeated_numbered() {
let out = rewrite_placeholders("INSERT INTO edges (source_id, target_id) VALUES (?1, ?1)");
assert_eq!(
out,
"INSERT INTO edges (source_id, target_id) VALUES ($1, $1)"
);
}
#[test]
fn rewrite_placeholders_mixed_bare_and_numbered() {
let out = rewrite_placeholders("SELECT * FROM t WHERE a = ?2 AND b = ? AND c = ?1");
assert_eq!(out, "SELECT * FROM t WHERE a = $2 AND b = $3 AND c = $1");
}
#[test]
fn numbered_placeholder_one_based() {
let p1 = numbered_placeholder(1);
let p3 = numbered_placeholder(3);
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
{
assert_eq!(p1, "?1");
assert_eq!(p3, "?3");
}
#[cfg(feature = "postgres")]
{
assert_eq!(p1, "$1");
assert_eq!(p3, "$3");
}
}
#[test]
fn placeholder_list_range() {
let list = placeholder_list(2, 3);
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
assert_eq!(list, "?2, ?3, ?4");
#[cfg(feature = "postgres")]
assert_eq!(list, "$2, $3, $4");
}
#[test]
fn placeholder_list_empty() {
assert_eq!(placeholder_list(1, 0), "");
}
}