use std::fmt::Write;
use fraiseql_error::{FraiseQLError, Result};
use super::super::where_generator::PostgresWhereGenerator;
use crate::{
dialect::{PostgresDialect, SqlDialect},
identifier::quote_postgres_identifier,
order_by::{Tiebreak, render_order_by_columns},
path_escape::escape_postgres_jsonb_segment,
traits::{
COMPOSED_DOCUMENT_KEY, COMPOSED_EMBEDS_KEY, ComposedEmbed, ComposedKeyset, ComposedLevel,
CursorValue, EmbedShape, EmbedSource, LevelKeys,
},
types::{DatabaseType, QueryParam},
};
const ORDINAL: &str = "\"_o\"";
pub(in super::super) fn build_composed_select_sql(
root: &ComposedLevel,
) -> Result<(String, Vec<QueryParam>)> {
let mut renderer = Renderer::default();
let level = renderer.level(root, LevelFrom::View(None))?;
Ok((
format!("SELECT _r.data FROM ({level}) AS _r ORDER BY _r.{ORDINAL}"),
renderer.params,
))
}
#[derive(Default)]
struct Renderer {
params: Vec<QueryParam>,
aliases: usize,
}
impl Renderer {
fn alias(&mut self) -> String {
let alias = format!("_l{}", self.aliases);
self.aliases += 1;
alias
}
fn bind(&mut self, param: QueryParam) -> String {
self.params.push(param);
format!("${}", self.params.len())
}
fn where_sql(
&mut self,
level: &ComposedLevel,
correlation: Option<&Correlation<'_>>,
) -> Result<String> {
let mut conditions = Vec::new();
if let Some(clause) = level.where_clause.as_ref() {
let generator = PostgresWhereGenerator::new(PostgresDialect);
let (sql, params) = generator.generate_with_param_offset(clause, self.params.len())?;
self.params.extend(params.into_iter().map(QueryParam::from));
conditions.push(sql);
}
if let Some(correlation) = correlation {
conditions.push(correlation.sql());
}
Ok(if conditions.is_empty() {
String::new()
} else {
format!(" WHERE {}", conditions.join(" AND "))
})
}
fn page(
&mut self,
level: &ComposedLevel,
correlation: Option<&Correlation<'_>>,
) -> Result<String> {
if let Some(keyset) = level.keyset.as_ref() {
return self.keyset_page(level, keyset, correlation);
}
let document = level.projection.as_deref().unwrap_or("data");
let mut sql = String::new();
let where_sql = self.where_sql(level, correlation)?;
let window = match render_order_by_columns(
level.order_by.as_deref(),
DatabaseType::PostgreSQL,
self.params.len() + 1,
Tiebreak::Identity,
)? {
Some(rendered) => {
self.params.extend(rendered.params.into_iter().map(QueryParam::Text));
format!("ORDER BY {}", rendered.columns)
},
None => String::new(),
};
write!(
sql,
"SELECT {document} AS data, row_number() OVER ({window}) AS {ORDINAL} FROM {}{where_sql} \
ORDER BY {ORDINAL}",
quote_postgres_identifier(&level.view),
)
.expect("write to String");
if let Some(limit) = level.limit {
let p = self.bind(QueryParam::BigInt(i64::from(limit)));
write!(sql, " LIMIT {p}").expect("write to String");
}
if let Some(offset) = level.offset {
let p = self.bind(QueryParam::BigInt(i64::from(offset)));
write!(sql, " OFFSET {p}").expect("write to String");
}
Ok(sql)
}
fn keyset_page(
&mut self,
level: &ComposedLevel,
keyset: &ComposedKeyset,
correlation: Option<&Correlation<'_>>,
) -> Result<String> {
if correlation.is_some() || level.offset.is_some() {
return Err(FraiseQLError::Validation {
message: format!(
"only the root of a composed read is paged by keyset, and not with an \
offset ('{}')",
level.view
),
path: None,
});
}
crate::order_by::refuse_relevance_under_cursor_pagination(level.order_by.as_deref())?;
let document = level.projection.as_deref().unwrap_or("data");
let column = quote_postgres_identifier(&keyset.cursor_column);
let direction = if keyset.forward { "ASC" } else { "DESC" };
let mut conditions = Vec::new();
let where_sql = self.where_sql(level, None)?;
if let Some(predicate) = where_sql.strip_prefix(" WHERE ") {
conditions.push(format!("({predicate})"));
}
let comparison = if keyset.forward { ">" } else { "<" };
match &keyset.cursor {
None => {},
Some(CursorValue::Int64(pk)) => {
let p = self.bind(QueryParam::BigInt(*pk));
conditions.push(format!("{column} {comparison} {p}"));
},
Some(CursorValue::Uuid(uuid)) => {
let p = self.bind(QueryParam::Text(uuid.clone()));
conditions.push(format!("{column} {comparison} {p}::uuid"));
},
}
let where_sql = if conditions.is_empty() {
String::new()
} else {
format!(" WHERE {}", conditions.join(" AND "))
};
let order = match render_order_by_columns(
level.order_by.as_deref(),
DatabaseType::PostgreSQL,
self.params.len() + 1,
Tiebreak::None,
)? {
Some(rendered) => {
self.params.extend(rendered.params.into_iter().map(QueryParam::Text));
format!("{}, {column} {direction}", rendered.columns)
},
None => format!("{column} {direction}"),
};
let limit = match level.limit {
Some(limit) => format!(" LIMIT {}", self.bind(QueryParam::BigInt(i64::from(limit)))),
None => String::new(),
};
let view = quote_postgres_identifier(&level.view);
Ok(if keyset.forward {
format!(
"SELECT {document} AS data, row_number() OVER (ORDER BY {order}) AS {ORDINAL} \
FROM {view}{where_sql} ORDER BY {ORDINAL}{limit}"
)
} else {
format!(
"SELECT _k.data AS data, row_number() OVER (ORDER BY _k._cursor ASC) AS {ORDINAL} \
FROM (SELECT {document} AS data, {column} AS _cursor FROM {view}{where_sql} \
ORDER BY {order}{limit}) AS _k"
)
})
}
fn materialised_page(
&mut self,
level: &ComposedLevel,
parent_alias: &str,
keys: &[String],
shape: EmbedShape,
) -> Result<String> {
if level.order_by.is_some() || level.projection.is_some() || level.keyset.is_some() {
return Err(FraiseQLError::Validation {
message: format!(
"a level read from its parent's document keeps the order it was stored \
in and carries no projection ('{}')",
level.view
),
path: None,
});
}
if level.where_clause.as_ref().is_some_and(reads_a_column) {
return Err(FraiseQLError::Validation {
message: format!(
"a level read from its parent's document has no columns, so its predicate \
may only read the document ('{}')",
level.view
),
path: None,
});
}
if keys.is_empty() {
return Err(FraiseQLError::Validation {
message: format!("a materialised level names no key ('{}')", level.view),
path: None,
});
}
let tried = keys
.iter()
.map(|k| format!("{parent_alias}.data->{}", literal(k)))
.collect::<Vec<_>>()
.join(", ");
let stored = format!("COALESCE({tried})");
let source = self.alias();
let from = match shape {
EmbedShape::Many => format!(
"jsonb_array_elements(CASE WHEN jsonb_typeof({stored}) = 'array' THEN {stored} \
ELSE '[]'::jsonb END) WITH ORDINALITY AS {source}(data, {ORDINAL})"
),
EmbedShape::One => {
format!("(SELECT {stored} AS data, 1::bigint AS {ORDINAL}) AS {source}")
},
EmbedShape::Count => {
return Err(FraiseQLError::Validation {
message: format!(
"a level read from its parent's document cannot be counted ('{}')",
level.view
),
path: None,
});
},
};
let mut where_sql = self.where_sql(level, None)?;
where_sql = if where_sql.is_empty() {
" WHERE jsonb_typeof(data) = 'object'".to_string()
} else {
format!("{where_sql} AND jsonb_typeof(data) = 'object'")
};
let mut sql = format!("SELECT data, {ORDINAL} FROM {from}{where_sql} ORDER BY {ORDINAL}");
if let Some(limit) = level.limit {
let p = self.bind(QueryParam::BigInt(i64::from(limit)));
write!(sql, " LIMIT {p}").expect("write to String");
}
if let Some(offset) = level.offset {
let p = self.bind(QueryParam::BigInt(i64::from(offset)));
write!(sql, " OFFSET {p}").expect("write to String");
}
Ok(sql)
}
fn level(&mut self, level: &ComposedLevel, from: LevelFrom<'_>) -> Result<String> {
let alias = self.alias();
let page = match from {
LevelFrom::View(correlation) => self.page(level, correlation)?,
LevelFrom::Parent {
alias: parent,
keys,
shape,
} => self.materialised_page(level, parent, keys, shape)?,
};
let document = document_sql(&alias, &level.keys);
let mut joins = String::new();
let mut embeds = Vec::with_capacity(level.embeds.len());
for (i, embed) in level.embeds.iter().enumerate() {
let embed_alias = format!("{alias}_e{i}");
let sql = self.embed(embed, &alias)?;
write!(joins, " LEFT JOIN LATERAL ({sql}) AS {embed_alias} ON true")
.expect("write to String");
embeds.push(format!("{}, {embed_alias}.v", literal(&embed.output_key)));
}
Ok(format!(
"SELECT jsonb_build_object({}, {document}, {}, jsonb_build_object({})) AS data, \
{alias}.{ORDINAL} AS {ORDINAL} FROM ({page}) AS {alias}{joins}",
literal(COMPOSED_DOCUMENT_KEY),
literal(COMPOSED_EMBEDS_KEY),
embeds.join(", "),
))
}
fn embed(&mut self, embed: &ComposedEmbed, parent_alias: &str) -> Result<String> {
let correlation;
let from = match &embed.source {
EmbedSource::Correlated {
target_key,
parent_key,
key_type,
} => {
correlation = Correlation {
target_key,
parent_alias,
parent_key,
key_type: *key_type,
};
LevelFrom::View(Some(&correlation))
},
EmbedSource::Materialised { keys } => LevelFrom::Parent {
alias: parent_alias,
keys,
shape: embed.shape,
},
};
match embed.shape {
EmbedShape::Many => {
let level = self.level(&embed.level, from)?;
Ok(format!(
"SELECT COALESCE(jsonb_agg(_c.data ORDER BY _c.{ORDINAL}), '[]'::jsonb) AS v \
FROM ({level}) AS _c"
))
},
EmbedShape::One => {
let level = self.level(&embed.level, from)?;
Ok(format!(
"SELECT _c.data AS v FROM ({level}) AS _c ORDER BY _c.{ORDINAL} LIMIT 1"
))
},
EmbedShape::Count => {
let LevelFrom::View(correlation) = from else {
return Err(FraiseQLError::Validation {
message: format!(
"a level read from its parent's document cannot be counted ('{}')",
embed.level.view
),
path: None,
});
};
let where_sql = self.where_sql(&embed.level, correlation)?;
Ok(format!(
"SELECT COUNT(*) AS v FROM {}{where_sql}",
quote_postgres_identifier(&embed.level.view)
))
},
}
}
}
#[derive(Clone, Copy)]
enum LevelFrom<'a> {
View(Option<&'a Correlation<'a>>),
Parent {
alias: &'a str,
keys: &'a [String],
shape: EmbedShape,
},
}
fn reads_a_column(clause: &crate::WhereClause) -> bool {
use crate::WhereClause as W;
match clause {
W::Field { .. } => false,
W::NativeField { .. } => true,
W::And(all) | W::Or(all) => all.iter().any(reads_a_column),
W::Not(inner) | W::Typed { inner, .. } | W::InHierarchy { inner, .. } => {
reads_a_column(inner)
},
W::Guarded { guard, inner, .. } => reads_a_column(guard) || reads_a_column(inner),
W::KeyIn { .. } => false,
}
}
fn document_sql(alias: &str, keys: &LevelKeys) -> String {
match keys {
LevelKeys::Whole => format!("{alias}.data"),
LevelKeys::Without(dropped) if dropped.is_empty() => format!("{alias}.data"),
LevelKeys::Without(dropped) => {
let list = dropped.iter().map(|k| literal(k)).collect::<Vec<_>>().join(", ");
format!("({alias}.data - ARRAY[{list}]::text[])")
},
LevelKeys::Only { kept, masked } => {
let kept_sql = if kept.is_empty() {
"'{}'::jsonb".to_string()
} else {
let list = kept.iter().map(|k| literal(k)).collect::<Vec<_>>().join(", ");
format!(
"(SELECT COALESCE(jsonb_object_agg(_k.key, _k.value), '{{}}'::jsonb) \
FROM jsonb_each({alias}.data) AS _k WHERE _k.key IN ({list}))"
)
};
if masked.is_empty() {
return kept_sql;
}
let masked_sql =
masked.iter().map(|k| format!("{}, NULL", literal(k))).collect::<Vec<_>>();
format!("({kept_sql} || jsonb_build_object({}))", masked_sql.join(", "))
},
}
}
fn literal(value: &str) -> String {
format!("'{}'", escape_postgres_jsonb_segment(value))
}
struct Correlation<'a> {
target_key: &'a [String],
parent_alias: &'a str,
parent_key: &'a [String],
key_type: crate::types::sql_hints::ScalarFieldType,
}
impl Correlation<'_> {
fn sql(&self) -> String {
let dialect = PostgresDialect;
let target = dialect.json_extract_scalar("data", self.target_key);
let parent =
dialect.json_extract_scalar(&format!("{}.data", self.parent_alias), self.parent_key);
format!(
"{} = {}",
dialect.cast_expr_as(&target, self.key_type),
dialect.cast_expr_as(&parent, self.key_type),
)
}
}