use crate::client::backend::{BackendError, BackendResult, PostgrestMethod, TranslatedQuery};
use crate::client::gateway_api::{
GatewayDeleteRequest, GatewayFetchRequest, GatewayInsertRequest, GatewayRequest,
GatewayRequestPayload, GatewayUpdateRequest,
};
use crate::client::query_builder::{Condition, ConditionOperator, OrderDirection};
use serde_json::Value;
use std::fmt::Write;
use std::iter::Peekable;
use supabase_rs::query::Query;
pub(crate) trait QueryTranslator {
fn translate_request(&self, request: &GatewayRequest) -> BackendResult<TranslatedQuery>;
}
pub struct SqlTranslator;
pub struct PostgrestTranslator;
pub struct CqlTranslator;
impl QueryTranslator for SqlTranslator {
fn translate_request(&self, request: &GatewayRequest) -> BackendResult<TranslatedQuery> {
match request.payload() {
GatewayRequestPayload::Fetch(query) => translate_sql_fetch(query),
GatewayRequestPayload::Insert(query) => translate_sql_insert(query),
GatewayRequestPayload::Update(query) => translate_sql_update(query),
GatewayRequestPayload::Delete(query) => translate_sql_delete(query),
GatewayRequestPayload::Sql(query) => {
Ok(TranslatedQuery::sql(&query.query, Vec::new(), None))
}
GatewayRequestPayload::Rpc(_) => Err(BackendError::Generic(
"SQL translator does not support RPC requests".to_string(),
)),
}
}
}
impl QueryTranslator for CqlTranslator {
fn translate_request(&self, request: &GatewayRequest) -> BackendResult<TranslatedQuery> {
match request.payload() {
GatewayRequestPayload::Fetch(query) => translate_cql_fetch(query),
GatewayRequestPayload::Insert(query) => translate_cql_insert(query),
GatewayRequestPayload::Update(query) => translate_cql_update(query),
GatewayRequestPayload::Delete(query) => translate_cql_delete(query),
GatewayRequestPayload::Sql(query) => {
Ok(TranslatedQuery::cql(&query.query, Vec::new(), None))
}
GatewayRequestPayload::Rpc(_) => Err(BackendError::Generic(
"CQL translator does not support RPC requests".to_string(),
)),
}
}
}
impl QueryTranslator for PostgrestTranslator {
fn translate_request(&self, request: &GatewayRequest) -> BackendResult<TranslatedQuery> {
match request.payload() {
GatewayRequestPayload::Fetch(query) => translate_postgrest_fetch(query),
GatewayRequestPayload::Insert(query) => translate_postgrest_insert(query),
GatewayRequestPayload::Update(query) => translate_postgrest_update(query),
GatewayRequestPayload::Delete(query) => translate_postgrest_delete(query),
GatewayRequestPayload::Sql(_) => Err(BackendError::Generic(
"PostgREST translator does not support raw SQL requests".to_string(),
)),
GatewayRequestPayload::Rpc(_) => Err(BackendError::Generic(
"PostgREST translator does not support RPC requests".to_string(),
)),
}
}
}
fn translate_sql_fetch(query: &GatewayFetchRequest) -> BackendResult<TranslatedQuery> {
let select_nodes: Vec<SelectNode> = parse_select(&query.raw_select, &query.columns)?;
let mut emitter: SqlEmitter = SqlEmitter::default();
emitter.emit_root(&query.table, select_nodes, query)
}
fn translate_sql_insert(query: &GatewayInsertRequest) -> BackendResult<TranslatedQuery> {
let (columns, values, params) = build_insert_fragments(&query.payload);
let sql: String = format!(
"INSERT INTO {} ({}) VALUES ({})",
query.table,
columns.join(", "),
values.join(", ")
);
Ok(TranslatedQuery::sql(sql, params, Some(query.table.clone())))
}
fn translate_sql_update(query: &GatewayUpdateRequest) -> BackendResult<TranslatedQuery> {
let (assignments, mut params) = build_update_fragments(&query.payload);
let mut sql: String = format!("UPDATE {} SET {}", query.table, assignments.join(", "));
let conditions: Vec<Condition> =
combined_conditions(&query.scope.conditions, &query.scope.row_id);
let (where_clause, where_params) = build_where_clause(&conditions);
sql.push_str(&where_clause);
params.extend(where_params);
Ok(TranslatedQuery::sql(sql, params, Some(query.table.clone())))
}
fn translate_sql_delete(query: &GatewayDeleteRequest) -> BackendResult<TranslatedQuery> {
let mut sql: String = format!("DELETE FROM {}", query.table);
let conditions: Vec<Condition> = combined_conditions(&query.conditions, &query.row_id);
let (where_clause, params) = build_where_clause(&conditions);
sql.push_str(&where_clause);
Ok(TranslatedQuery::sql(sql, params, Some(query.table.clone())))
}
fn translate_cql_fetch(query: &GatewayFetchRequest) -> BackendResult<TranslatedQuery> {
let columns: String = if query.columns.is_empty() {
"*".to_string()
} else {
query.columns.join(", ")
};
let mut sql: String = format!("SELECT {} FROM {}", columns, query.table);
let (where_clause, params) = build_where_clause(&query.conditions);
sql.push_str(&where_clause);
if let Some(limit) = query.limit {
sql.push_str(&format!(" LIMIT {}", limit));
}
Ok(TranslatedQuery::cql(sql, params, Some(query.table.clone())))
}
fn translate_cql_insert(query: &GatewayInsertRequest) -> BackendResult<TranslatedQuery> {
let (columns, values, params) = build_insert_fragments(&query.payload);
let sql: String = format!(
"INSERT INTO {} ({}) VALUES ({})",
query.table,
columns.join(", "),
values.join(", ")
);
Ok(TranslatedQuery::cql(sql, params, Some(query.table.clone())))
}
fn translate_cql_update(query: &GatewayUpdateRequest) -> BackendResult<TranslatedQuery> {
let (assignments, mut params) = build_update_fragments(&query.payload);
let mut sql: String = format!("UPDATE {} SET {}", query.table, assignments.join(", "));
let conditions: Vec<Condition> =
combined_conditions(&query.scope.conditions, &query.scope.row_id);
let (where_clause, where_params) = build_where_clause(&conditions);
sql.push_str(&where_clause);
params.extend(where_params);
Ok(TranslatedQuery::cql(sql, params, Some(query.table.clone())))
}
fn translate_cql_delete(query: &GatewayDeleteRequest) -> BackendResult<TranslatedQuery> {
let mut sql: String = format!("DELETE FROM {}", query.table);
let conditions: Vec<Condition> = combined_conditions(&query.conditions, &query.row_id);
let (where_clause, params) = build_where_clause(&conditions);
sql.push_str(&where_clause);
Ok(TranslatedQuery::cql(sql, params, Some(query.table.clone())))
}
fn translate_postgrest_fetch(query: &GatewayFetchRequest) -> BackendResult<TranslatedQuery> {
let mut q: Query = Query::new();
let mut params: Vec<Value> = Vec::new();
let select: String = if let Some(raw) = &query.raw_select {
raw.clone()
} else if query.columns.is_empty() {
"*".to_string()
} else {
query.columns.join(",")
};
q.add_param("select", &select);
for condition in &query.conditions {
let encoded: String = postgrest_encoded_condition(condition)?;
q.add_param(&condition.column, &encoded);
params.push(postgrest_condition_param_value(condition)?);
}
for (column, direction) in &query.order_by {
let dir_str: &str = match direction {
OrderDirection::Asc => "asc",
OrderDirection::Desc => "desc",
};
q.add_param("order", &format!("{column}.{dir_str}"));
}
if let Some(limit) = query.limit {
q.add_param("limit", &limit.to_string());
}
if let Some(offset) = query.offset {
q.add_param("offset", &offset.to_string());
}
Ok(
TranslatedQuery::postgrest(q.build(), params, Some(query.table.clone()))
.with_postgrest_method(PostgrestMethod::Get),
)
}
fn translate_postgrest_insert(query: &GatewayInsertRequest) -> BackendResult<TranslatedQuery> {
Ok(
TranslatedQuery::postgrest("", Vec::new(), Some(query.table.clone()))
.with_postgrest_method(PostgrestMethod::Post)
.with_payload(query.payload.clone()),
)
}
fn translate_postgrest_update(query: &GatewayUpdateRequest) -> BackendResult<TranslatedQuery> {
let conditions: Vec<Condition> =
combined_conditions(&query.scope.conditions, &query.scope.row_id);
if conditions.is_empty() {
return Err(BackendError::Generic(
"PostgREST update requires at least one filter condition".to_string(),
));
}
let mut q: Query = Query::new();
let mut params: Vec<Value> = Vec::new();
for condition in &conditions {
let encoded: String = postgrest_encoded_condition(condition)?;
q.add_param(&condition.column, &encoded);
params.push(postgrest_condition_param_value(condition)?);
}
Ok(
TranslatedQuery::postgrest(q.build(), params, Some(query.table.clone()))
.with_postgrest_method(PostgrestMethod::Patch)
.with_payload(query.payload.clone()),
)
}
fn translate_postgrest_delete(query: &GatewayDeleteRequest) -> BackendResult<TranslatedQuery> {
let conditions: Vec<Condition> = combined_conditions(&query.conditions, &query.row_id);
if conditions.is_empty() {
return Err(BackendError::Generic(
"PostgREST delete requires at least one filter condition".to_string(),
));
}
let mut q: Query = Query::new();
let mut params: Vec<Value> = Vec::new();
for condition in &conditions {
let encoded: String = postgrest_encoded_condition(condition)?;
q.add_param(&condition.column, &encoded);
params.push(postgrest_condition_param_value(condition)?);
}
Ok(
TranslatedQuery::postgrest(q.build(), params, Some(query.table.clone()))
.with_postgrest_method(PostgrestMethod::Delete),
)
}
fn combined_conditions(conditions: &[Condition], row_id: &Option<String>) -> Vec<Condition> {
let mut merged: Vec<Condition> = conditions.to_vec();
if let Some(row_id) = row_id {
merged.push(Condition::new(
"id",
ConditionOperator::Eq,
vec![Value::String(row_id.clone())],
));
}
merged
}
fn build_where_clause(conditions: &[Condition]) -> (String, Vec<Value>) {
if conditions.is_empty() {
return (String::new(), Vec::new());
}
let mut clauses: Vec<String> = Vec::new();
let mut params: Vec<Value> = Vec::new();
for condition in conditions {
match condition.operator {
ConditionOperator::In => {
let formatted: Vec<String> = condition.values.iter().map(format_value).collect();
clauses.push(format!(
"{} IN ({})",
condition.column,
formatted.join(", ")
));
params.extend(condition.values.clone());
}
_ => {
if let Some(value) = condition.values.first() {
let operator = match condition.operator {
ConditionOperator::Eq => "=",
ConditionOperator::Neq => "<>",
ConditionOperator::Gt => ">",
ConditionOperator::Lt => "<",
_ => "=",
};
clauses.push(format!(
"{} {} {}",
condition.column,
operator,
format_value(value)
));
params.extend(condition.values.clone());
}
}
}
}
(format!(" WHERE {}", clauses.join(" AND ")), params)
}
fn build_insert_fragments(payload: &Value) -> (Vec<String>, Vec<String>, Vec<Value>) {
if let Value::Object(map) = payload {
let mut columns: Vec<String> = Vec::new();
let mut values: Vec<String> = Vec::new();
let mut params: Vec<Value> = Vec::new();
for (column, value) in map {
columns.push(column.clone());
values.push(format_value(value));
params.push(value.clone());
}
(columns, values, params)
} else {
(Vec::new(), Vec::new(), Vec::new())
}
}
fn build_update_fragments(payload: &Value) -> (Vec<String>, Vec<Value>) {
if let Value::Object(map) = payload {
let mut assignments: Vec<String> = Vec::new();
let mut params: Vec<Value> = Vec::new();
for (column, value) in map {
assignments.push(format!("{} = {}", column, format_value(value)));
params.push(value.clone());
}
(assignments, params)
} else {
(Vec::new(), Vec::new())
}
}
fn format_value(value: &Value) -> String {
match value {
Value::String(text) => format!("'{}'", text.replace('\'', "''")),
Value::Number(num) => num.to_string(),
Value::Bool(flag) => flag.to_string(),
Value::Null => "NULL".to_string(),
other => serde_json::to_string(other).unwrap_or_else(|_| "NULL".to_string()),
}
}
fn postgrest_operator(op: ConditionOperator, value: &Value) -> BackendResult<String> {
let val: String = match value {
Value::String(s) => s.clone(),
other => other.to_string(),
};
let encoded: String = match op {
ConditionOperator::Eq => format!("eq.{val}"),
ConditionOperator::Neq => format!("neq.{val}"),
ConditionOperator::Gt => format!("gt.{val}"),
ConditionOperator::Lt => format!("lt.{val}"),
ConditionOperator::In => format!("in.({val})"),
};
Ok(encoded)
}
fn postgrest_encoded_condition(condition: &Condition) -> BackendResult<String> {
match condition.operator {
ConditionOperator::In => {
let joined: String = condition
.values
.iter()
.map(|value| match value {
Value::String(s) => s.clone(),
other => other.to_string(),
})
.collect::<Vec<String>>()
.join(",");
postgrest_operator(ConditionOperator::In, &Value::String(joined))
}
_ => {
let value: &Value = condition.values.first().ok_or_else(|| {
BackendError::Generic(format!(
"missing value for condition on column '{}'",
condition.column
))
})?;
postgrest_operator(condition.operator, value)
}
}
}
fn postgrest_condition_param_value(condition: &Condition) -> BackendResult<Value> {
match condition.operator {
ConditionOperator::In => Ok(Value::Array(condition.values.clone())),
_ => condition.values.first().cloned().ok_or_else(|| {
BackendError::Generic(format!(
"missing value for condition on column '{}'",
condition.column
))
}),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum JoinKind {
Left,
Inner,
}
#[derive(Debug, Clone)]
enum SelectNode {
Column(String),
Relation(RelationNode),
}
#[derive(Debug, Clone)]
struct RelationNode {
name: String,
alias: Option<String>,
join: JoinKind,
foreign_key: Option<String>,
children: Vec<SelectNode>,
conditions: Vec<Condition>,
}
impl RelationNode {
fn display_name(&self) -> &str {
self.alias.as_deref().unwrap_or(&self.name)
}
}
fn parse_select(raw: &Option<String>, columns: &[String]) -> BackendResult<Vec<SelectNode>> {
if let Some(raw) = raw {
parse_select_string(raw)
} else {
Ok(columns
.iter()
.map(|c| SelectNode::Column(c.clone()))
.collect())
}
}
fn parse_select_string(input: &str) -> BackendResult<Vec<SelectNode>> {
let mut chars = input.chars().peekable();
parse_nodes(&mut chars)
}
fn parse_nodes<I>(iter: &mut Peekable<I>) -> BackendResult<Vec<SelectNode>>
where
I: Iterator<Item = char>,
{
let mut nodes: Vec<SelectNode> = Vec::new();
loop {
skip_ws(iter);
if matches!(iter.peek(), Some(')')) || iter.peek().is_none() {
break;
}
nodes.push(parse_node(iter)?);
skip_ws(iter);
if matches!(iter.peek(), Some(',')) {
iter.next();
continue;
}
if matches!(iter.peek(), Some(')')) || iter.peek().is_none() {
break;
}
}
Ok(nodes)
}
fn parse_node<I>(iter: &mut Peekable<I>) -> BackendResult<SelectNode>
where
I: Iterator<Item = char>,
{
let mut token: String = String::new();
while let Some(&ch) = iter.peek() {
if matches!(ch, '!' | ':' | '(' | ')' | ',') {
break;
}
token.push(ch);
iter.next();
}
let token: String = token.trim().to_string();
if token.is_empty() {
return Err(BackendError::Generic(
"invalid select syntax: empty token".to_string(),
));
}
let mut alias: Option<String> = None;
let mut name_owned: String = token.clone();
if matches!(iter.peek(), Some(':')) {
iter.next();
let mut relation: String = String::new();
while let Some(&ch) = iter.peek() {
if matches!(ch, '!' | '(' | ')' | ',') {
break;
}
relation.push(ch);
iter.next();
}
alias = Some(token.clone());
name_owned = relation;
}
let name: &str = name_owned.as_str();
let mut join: JoinKind = JoinKind::Left;
let mut foreign_key: Option<String> = None;
if matches!(iter.peek(), Some('!')) {
iter.next();
let mut modifier: String = String::new();
while let Some(&ch) = iter.peek() {
if matches!(ch, '(' | ')' | ',') {
break;
}
modifier.push(ch);
iter.next();
}
let modifier: &str = modifier.trim();
if modifier.eq_ignore_ascii_case("inner") {
join = JoinKind::Inner;
} else if !modifier.is_empty() {
foreign_key = Some(modifier.to_string());
}
}
if matches!(iter.peek(), Some('(')) {
iter.next();
let children: Vec<SelectNode> = parse_nodes(iter)?;
if iter.next() != Some(')') {
return Err(BackendError::Generic(
"invalid select syntax: missing ')'".to_string(),
));
}
Ok(SelectNode::Relation(RelationNode {
name: name.to_string(),
alias,
join,
foreign_key,
children,
conditions: Vec::new(),
}))
} else {
Ok(SelectNode::Column(name.to_string()))
}
}
fn skip_ws<I>(iter: &mut Peekable<I>)
where
I: Iterator<Item = char>,
{
while matches!(iter.peek(), Some(ch) if ch.is_whitespace()) {
iter.next();
}
}
fn distribute_conditions(nodes: &mut [SelectNode], conditions: &[Condition]) -> Vec<Condition> {
let mut remaining: Vec<Condition> = Vec::new();
for cond in conditions {
if let Some((head, rest)) = cond.column.split_once('.')
&& let Some(node) = nodes.iter_mut().find_map(|n| match n {
SelectNode::Relation(r) if r.display_name() == head => Some(r),
_ => None,
})
{
push_condition(node, rest, cond.clone());
continue;
}
remaining.push(cond.clone());
}
remaining
}
fn push_condition(target: &mut RelationNode, path: &str, condition: Condition) {
if let Some((head, rest)) = path.split_once('.') {
if let Some(child) = target.children.iter_mut().find_map(|n| match n {
SelectNode::Relation(r) if r.display_name() == head => Some(r),
_ => None,
}) {
push_condition(child, rest, condition);
}
} else {
target.conditions.push(condition);
}
}
#[derive(Default)]
struct SqlEmitter {
alias_counter: usize,
}
impl SqlEmitter {
fn emit_root(
&mut self,
table: &str,
mut nodes: Vec<SelectNode>,
query: &GatewayFetchRequest,
) -> BackendResult<TranslatedQuery> {
let base_alias: String = "t0".to_string();
let base_conditions: Vec<Condition> = distribute_conditions(&mut nodes, &query.conditions);
let mut select_exprs: Vec<String> = Vec::new();
let mut joins: Vec<String> = Vec::new();
for node in nodes {
self.emit_node(&node, &base_alias, table, &mut select_exprs, &mut joins)?;
}
if select_exprs.is_empty() {
select_exprs.push("*".to_string());
}
let mut sql: String = format!(
"SELECT {select} FROM {table} {alias}",
select = select_exprs.join(", "),
table = table,
alias = base_alias
);
if !joins.is_empty() {
sql.push('\n');
sql.push_str(&joins.join("\n"));
}
if !base_conditions.is_empty() {
let adapted: Vec<Condition> = base_conditions
.iter()
.cloned()
.map(|mut c| {
c.column = format!("{base_alias}.{}", c.column);
c
})
.collect();
let (where_clause, _) = build_where_clause(&adapted);
sql.push_str(&where_clause);
}
if !query.order_by.is_empty() {
let ordering: Vec<String> = query
.order_by
.iter()
.map(|(column, direction)| {
let dir_str: &str = match direction {
OrderDirection::Asc => "ASC",
OrderDirection::Desc => "DESC",
};
if column.contains('.') {
format!("{column} {dir_str}")
} else {
format!("{base_alias}.{column} {dir_str}")
}
})
.collect();
sql.push_str(&format!(" ORDER BY {}", ordering.join(", ")));
}
if let Some(limit) = query.limit {
sql.push_str(&format!(" LIMIT {}", limit));
}
if let Some(offset) = query.offset {
sql.push_str(&format!(" OFFSET {}", offset));
}
Ok(TranslatedQuery::sql(
sql,
Vec::new(),
Some(table.to_string()),
))
}
fn emit_node(
&mut self,
node: &SelectNode,
parent_alias: &str,
parent_table: &str,
select_exprs: &mut Vec<String>,
joins: &mut Vec<String>,
) -> BackendResult<()> {
match node {
SelectNode::Column(col) => {
if col == "*" {
select_exprs.push(format!("{parent_alias}.*"));
} else {
select_exprs.push(format!("{parent_alias}.{col}"));
}
}
SelectNode::Relation(rel) => {
let alias: String = self.next_alias(&rel.name)?;
let agg_alias: String = format!("{alias}_agg");
let mut relation_clone: RelationNode = rel.clone();
let _ = distribute_conditions(&mut relation_clone.children, &rel.conditions);
let (row_expr, child_joins) = self.emit_relation_row(&relation_clone, &alias)?;
let (join_condition, many_to_one) =
relation_join_condition(&relation_clone, parent_alias, parent_table, &alias);
let mut subquery: String = String::new();
if many_to_one {
writeln!(
subquery,
"SELECT {row_expr} AS data FROM {table} {alias}",
table = relation_clone.name,
alias = alias,
row_expr = row_expr
)
.unwrap();
} else {
write!(
subquery,
"SELECT COALESCE(jsonb_agg({row_expr} ORDER BY {alias}.id) FILTER (WHERE {alias}.id IS NOT NULL), '[]'::jsonb) AS data\nFROM {table} {alias}\n",
row_expr = row_expr,
alias = alias,
table = relation_clone.name
)
.unwrap();
}
if !child_joins.is_empty() {
subquery.push_str(&child_joins.join("\n"));
subquery.push('\n');
}
write!(subquery, "WHERE {join_condition}").unwrap();
if !relation_clone.conditions.is_empty() {
let adapted: Vec<Condition> = relation_clone
.conditions
.iter()
.cloned()
.map(|mut c| {
c.column = format!("{alias}.{}", c.column);
c
})
.collect();
let (where_clause, _) = build_where_clause(&adapted);
subquery.push_str(&where_clause);
}
if many_to_one {
subquery.push_str(" LIMIT 1");
}
let join_type: &str = match rel.join {
JoinKind::Left => "LEFT JOIN",
JoinKind::Inner => "JOIN",
};
let mut join_sql: String =
format!("{join_type} LATERAL (\n{subquery}\n) {agg_alias} ON ");
if rel.join == JoinKind::Inner {
join_sql.push_str(&format!("{agg_alias}.data IS NOT NULL"));
} else {
join_sql.push_str("TRUE");
}
joins.push(join_sql);
select_exprs.push(format!("{} as {}", agg_alias, rel.display_name()));
}
}
Ok(())
}
fn emit_relation_row(
&mut self,
relation: &RelationNode,
alias: &str,
) -> BackendResult<(String, Vec<String>)> {
let mut field_pairs: Vec<String> = Vec::new();
let mut joins: Vec<String> = Vec::new();
for child in &relation.children {
match child {
SelectNode::Column(col) => {
field_pairs.push(format!("'{}', {}.{}", col, alias, col));
}
SelectNode::Relation(rel) => {
let child_alias: String = self.next_alias(&rel.name)?;
let mut rel_clone: RelationNode = rel.clone();
let _ = distribute_conditions(&mut rel_clone.children, &rel.conditions);
let (row_expr, nested_joins) =
self.emit_relation_row(&rel_clone, &child_alias)?;
let (join_condition, many_to_one) =
relation_join_condition(&rel_clone, alias, &relation.name, &child_alias);
let mut subquery: String = String::new();
if many_to_one {
writeln!(
subquery,
"SELECT {row_expr} AS data FROM {table} {child_alias}",
row_expr = row_expr,
table = rel_clone.name,
child_alias = child_alias
)
.unwrap();
} else {
write!(
subquery,
"SELECT COALESCE(jsonb_agg({row_expr} ORDER BY {child_alias}.id) FILTER (WHERE {child_alias}.id IS NOT NULL), '[]'::jsonb) AS data\nFROM {table} {child_alias}\n",
row_expr = row_expr,
child_alias = child_alias,
table = rel_clone.name
)
.unwrap();
}
if !nested_joins.is_empty() {
subquery.push_str(&nested_joins.join("\n"));
subquery.push('\n');
}
write!(subquery, "WHERE {join_condition}").unwrap();
if !rel_clone.conditions.is_empty() {
let adapted: Vec<Condition> = rel_clone
.conditions
.iter()
.cloned()
.map(|mut c| {
c.column = format!("{child_alias}.{}", c.column);
c
})
.collect();
let (where_clause, _) = build_where_clause(&adapted);
subquery.push_str(&where_clause);
}
if many_to_one {
subquery.push_str(" LIMIT 1");
}
let join_type: &str = match rel.join {
JoinKind::Left => "LEFT JOIN",
JoinKind::Inner => "JOIN",
};
let agg_alias: String = format!("{child_alias}_agg");
let mut join_sql: String =
format!("{join_type} LATERAL (\n{subquery}\n) {agg_alias} ON ");
if rel.join == JoinKind::Inner {
join_sql.push_str(&format!("{agg_alias}.data IS NOT NULL"));
} else {
join_sql.push_str("TRUE");
}
joins.push(join_sql);
field_pairs.push(format!("'{}', {agg_alias}.data", rel.display_name()));
}
}
}
let row_expr: String = if field_pairs.is_empty() {
format!("to_jsonb({alias})")
} else {
format!("jsonb_build_object({})", field_pairs.join(", "))
};
Ok((row_expr, joins))
}
fn next_alias(&mut self, base: &str) -> BackendResult<String> {
let alias: String = format!("{}_{}", base, self.alias_counter);
self.alias_counter += 1;
Ok(alias)
}
}
fn relation_join_condition(
relation: &RelationNode,
parent_alias: &str,
parent_table: &str,
child_alias: &str,
) -> (String, bool) {
if let Some(fk) = &relation.foreign_key {
let fk_lower: String = fk.to_lowercase();
if let Some(stripped) = fk_lower.strip_prefix("parent.") {
return (
format!("{parent_alias}.{stripped} = {child_alias}.id"),
true,
);
}
if let Some(stripped) = fk_lower.strip_prefix("child.") {
return (
format!("{child_alias}.{stripped} = {parent_alias}.id"),
false,
);
}
let rel_singular: String = relation.name.trim_end_matches('s').to_lowercase();
let rel_lower: String = relation.name.to_lowercase();
let fk_suggests_parent_side: bool =
fk_lower.contains(&rel_lower) || fk_lower.contains(&rel_singular);
return if fk_suggests_parent_side {
(format!("{parent_alias}.{fk} = {child_alias}.id"), true)
} else {
(format!("{child_alias}.{fk} = {parent_alias}.id"), false)
};
}
let child_fk: String = format!("{}_id", parent_table.trim_end_matches('s'));
let parent_fk: String = format!("{}_id", relation.name.trim_end_matches('s'));
let many_to_one: bool = relation.name.len() >= parent_table.len();
if many_to_one {
(
format!("{parent_alias}.{parent_fk} = {child_alias}.id"),
true,
)
} else {
(
format!("{child_alias}.{child_fk} = {parent_alias}.id"),
false,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::QueryLanguage;
use serde_json::json;
fn translate<T>(
translator: &T,
request: impl Into<GatewayRequest>,
) -> BackendResult<TranslatedQuery>
where
T: QueryTranslator,
{
let request: GatewayRequest = request.into();
translator.translate_request(&request)
}
#[test]
fn parses_nested_supabase_select() {
let select: &str = "id,name,instruments!inner(id,name),start_scan:scans!scan_id_start(id)";
let nodes: Vec<SelectNode> =
parse_select(&Some(select.to_string()), &[]).expect("parse ok");
assert_eq!(nodes.len(), 4);
match &nodes[2] {
SelectNode::Relation(rel) => {
assert_eq!(rel.name, "instruments");
assert!(matches!(rel.join, JoinKind::Inner));
assert_eq!(rel.children.len(), 2);
}
_ => panic!("expected relation"),
}
}
#[test]
fn postgrest_translator_builds_query_string() {
let translator: PostgrestTranslator = PostgrestTranslator;
let request: GatewayFetchRequest = GatewayFetchRequest::new("orchestral_sections")
.raw_select("id,name,instruments(id,name)")
.where_condition(Condition::new(
"instruments.name",
ConditionOperator::Eq,
vec![Value::String("flute".to_string())],
))
.limit(10)
.offset(5);
let translated: TranslatedQuery = translate(&translator, request).expect("translate ok");
assert_eq!(translated.language, QueryLanguage::Postgrest);
assert_eq!(translated.postgrest_method, Some(PostgrestMethod::Get));
assert!(
translated
.sql
.contains("select=id,name,instruments(id,name)")
);
assert!(translated.sql.contains("instruments.name=eq.flute"));
assert!(translated.sql.contains("limit=10"));
assert!(translated.sql.contains("offset=5"));
}
#[test]
fn postgrest_translator_builds_update_with_filters_and_payload() {
let translator: PostgrestTranslator = PostgrestTranslator;
let request: crate::client::gateway_api::GatewayMutationRequest<
crate::client::GatewayUpdateScope,
> = GatewayUpdateRequest::new(
"users",
json!({
"status": "active"
}),
)
.where_condition(Condition::new(
"email",
ConditionOperator::Eq,
vec![Value::String("alice@example.com".to_string())],
));
let translated = translate(&translator, request).expect("translate ok");
assert_eq!(translated.language, QueryLanguage::Postgrest);
assert_eq!(translated.postgrest_method, Some(PostgrestMethod::Patch));
assert_eq!(translated.table.as_deref(), Some("users"));
assert!(translated.sql.contains("email=eq.alice@example.com"));
assert_eq!(translated.payload, Some(json!({ "status": "active" })));
}
#[test]
fn postgrest_translator_rejects_unfiltered_delete() {
let translator = PostgrestTranslator;
let request = GatewayDeleteRequest::new("users");
let err = translate(&translator, request).expect_err("delete without filters should fail");
assert!(
err.to_string()
.contains("requires at least one filter condition")
);
}
#[test]
fn sql_translator_builds_lateral_join() {
let translator = SqlTranslator;
let request = GatewayFetchRequest::new("orchestral_sections")
.raw_select("id,name,instruments(id,name)");
let translated = translate(&translator, request).expect("translate ok");
assert_eq!(translated.language, QueryLanguage::Sql);
assert!(translated.sql.contains("jsonb_agg"));
assert!(translated.sql.contains("FROM instruments instruments_0"));
assert!(translated.sql.contains("instruments_0_agg as instruments"));
}
#[test]
fn sql_translator_child_side_fk_uses_jsonb_agg() {
let translator = SqlTranslator;
let request =
GatewayFetchRequest::new("authors").raw_select("id,name,posts!author_id(id,title)");
let translated = translate(&translator, request).expect("translate ok");
assert_eq!(translated.language, QueryLanguage::Sql);
assert!(
translated.sql.contains("posts_0.author_id = t0.id"),
"expected child-side join condition, got: {}",
translated.sql
);
assert!(
translated.sql.contains("jsonb_agg"),
"expected jsonb_agg for one-to-many, got: {}",
translated.sql
);
assert!(
!translated.sql.contains("LIMIT 1"),
"one-to-many should not use LIMIT 1, got: {}",
translated.sql
);
}
#[test]
fn sql_update_translator_combines_builder_conditions_and_row_id() {
let translator = SqlTranslator;
let request = GatewayUpdateRequest::new(
"users",
json!({
"name": "Alice"
}),
)
.row_id("user_42")
.where_condition(Condition::new(
"status",
ConditionOperator::Eq,
vec![Value::String("active".to_string())],
));
let translated = translate(&translator, request).expect("translate ok");
assert_eq!(translated.language, QueryLanguage::Sql);
assert_eq!(
translated.sql,
"UPDATE users SET name = 'Alice' WHERE status = 'active' AND id = 'user_42'"
);
}
#[test]
fn sql_delete_translator_supports_condition_filters_without_row_id() {
let translator = SqlTranslator;
let request = GatewayDeleteRequest::new("users")
.where_condition(Condition::new(
"age",
ConditionOperator::Gt,
vec![Value::from(18)],
))
.where_condition(Condition::new(
"status",
ConditionOperator::Neq,
vec![Value::String("archived".to_string())],
));
let translated = translate(&translator, request).expect("translate ok");
assert_eq!(translated.language, QueryLanguage::Sql);
assert_eq!(
translated.sql,
"DELETE FROM users WHERE age > 18 AND status <> 'archived'"
);
}
}