use std::collections::HashMap;
use schema_core::{ColumnName, DatabaseSchema, GenericValue, IndexSchema, TableName};
use sources_core::{Result, SourceError};
use sqlx::Postgres;
use sqlx::postgres::PgArguments;
use builder::Builder;
use sql::{qcol, qident, qtable};
mod builder;
mod sql;
#[cfg(test)]
mod proptests;
#[cfg(test)]
mod tests;
type PgQuery<'q> = sqlx::query::Query<'q, Postgres, PgArguments>;
const ROOT: &str = "root";
#[derive(Debug, Clone)]
pub(super) struct SqlString(String);
impl SqlString {
fn new(sql: String) -> Self {
Self(sql)
}
#[cfg(test)]
pub(super) fn as_str(&self) -> &str {
&self.0
}
}
impl sqlx::SqlSafeStr for SqlString {
fn into_sql_str(self) -> sqlx::SqlStr {
sqlx::AssertSqlSafe(self.0).into_sql_str()
}
}
pub(super) fn bind_param<'q>(query: PgQuery<'q>, value: &GenericValue) -> Result<PgQuery<'q>> {
Ok(match value {
GenericValue::Int(i) => query.bind(*i),
GenericValue::Bool(b) => query.bind(*b),
GenericValue::Decimal(d) => query.bind(*d),
GenericValue::String(s) => query.bind(s.clone()),
GenericValue::Null | GenericValue::Array(_) | GenericValue::Map(_) => {
return Err(SourceError::Query(
"cannot bind null, array, or map as a parameter".into(),
));
}
})
}
pub(super) fn document_query(
schema: &IndexSchema,
key: &[(ColumnName, GenericValue)],
pks: &HashMap<String, ColumnName>,
col_types: &HashMap<(String, String), String>,
) -> Result<(SqlString, Vec<GenericValue>)> {
let mut builder = Builder {
db: &schema.db_schema,
pks,
col_types,
params: Vec::new(),
seq: 0,
};
let object = builder.object(&schema.fields, ROOT, schema.primary_key.as_ref())?;
let mut conditions = Vec::new();
for (column, value) in key {
let placeholder = builder.placeholder(value.clone())?;
conditions.push(format!("{} = {placeholder}", qcol(ROOT, column)));
}
if let Some(predicate) = builder.soft_delete_predicate(schema)? {
conditions.push(format!("NOT ({predicate})"));
}
if conditions.is_empty() {
conditions.push("true".to_owned());
}
let root_filters = builder.filters(schema.filters.as_deref(), ROOT, &schema.table)?;
let sql = format!(
"SELECT {object} AS \"document\" FROM {} AS \"{ROOT}\" WHERE {}{root_filters}",
qtable(&schema.db_schema, &schema.table),
conditions.join(" AND "),
);
Ok((SqlString::new(sql), builder.params))
}
pub(super) fn documents_query(
schema: &IndexSchema,
pk_column: &ColumnName,
keys: &[GenericValue],
pks: &HashMap<String, ColumnName>,
col_types: &HashMap<(String, String), String>,
) -> Result<(SqlString, Vec<GenericValue>)> {
let mut builder = Builder {
db: &schema.db_schema,
pks,
col_types,
params: Vec::new(),
seq: 0,
};
let object = builder.object(&schema.fields, ROOT, schema.primary_key.as_ref())?;
let mut placeholders = Vec::with_capacity(keys.len());
for key in keys {
placeholders.push(builder.placeholder(key.clone())?);
}
let mut predicate = format!("{} IN ({})", qcol(ROOT, pk_column), placeholders.join(", "),);
if let Some(soft_delete) = builder.soft_delete_predicate(schema)? {
predicate = format!("{predicate} AND NOT ({soft_delete})");
}
let root_filters = builder.filters(schema.filters.as_deref(), ROOT, &schema.table)?;
predicate.push_str(&root_filters);
let sql = format!(
"SELECT to_json({key}) AS \"doc_key\", {object} AS \"document\" \
FROM {} AS \"{ROOT}\" WHERE {predicate}",
qtable(&schema.db_schema, &schema.table),
key = qcol(ROOT, pk_column),
);
Ok((SqlString::new(sql), builder.params))
}
pub(super) fn reverse_query(
db: &DatabaseSchema,
table: &TableName,
select_column: &ColumnName,
key: &[(ColumnName, GenericValue)],
) -> Result<(SqlString, Vec<GenericValue>)> {
let mut params = Vec::new();
let mut conditions = Vec::new();
for (column, value) in key {
if !value.is_bindable_scalar() {
return Err(SourceError::Query(
"cannot bind null, array, or map as a key".into(),
));
}
params.push(value.clone());
conditions.push(format!("{} = ${}", qident(column.as_ref()), params.len()));
}
if conditions.is_empty() {
conditions.push("true".to_owned());
}
let sql = format!(
"SELECT {} FROM {} WHERE {}",
qident(select_column.as_ref()),
qtable(db, table),
conditions.join(" AND "),
);
Ok((SqlString::new(sql), params))
}