use crate::ir::{
IrArraySource, IrCteDef, IrDelete, IrExpr, IrFor, IrForIterator, IrFreeExpr, IrFtsSearch, IrFunctionSelect,
IrGlobalCte, IrGroup, IrGroupOutput, IrInsert, IrLiteral, IrLockClause, IrLockStrength, IrLockWait,
IrMultiLinkJoin, IrMultiLinkMutation, IrMultiLinkPointer, IrMultiLinkValueSource, IrMultiLinkValues, IrNulls,
IrOutput, IrPathJoin, IrPathResult, IrPathSelect, IrPolyFanout, IrPolyImplementor, IrRewrite, IrRowSource,
IrScalarPointer, IrScalarSetPointer, IrSelect, IrShapePointer, IrSingleLinkCorrelation, IrSingleLinkPointer,
IrSort, IrSortDir, IrSource, IrStmt, IrUpdate, IrVectorSearch, SearchEnqueueInfo, VectorEnqueueInfo,
};
use crate::parse::ast::{BinOpKind, UnaryOpKind};
use crate::query::{Cardinality, InferencePlan, ShapeDescriptor, ShapeNode};
use std::collections::HashMap;
pub struct SqlOutput {
pub sql: String,
pub shape: ShapeDescriptor,
pub inference_plan: Option<InferencePlan>,
}
fn update_set_fragments(assignments: &[(String, IrExpr)], rewrites: &[IrRewrite], indent: &str) -> Vec<String> {
let rewrite_cols: std::collections::HashSet<&str> = rewrites.iter().map(|r| r.column.as_str()).collect();
assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(col, expr)| format!("{indent}{} = {}", qi(col), emit_expr(expr)))
.chain(
rewrites
.iter()
.map(|rw| format!("{indent}{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
)
.collect()
}
fn emit_for_global_cte(stmt: &IrStmt) -> String {
match stmt {
IrStmt::Select(sel) => match sel.rows.as_slice() {
[IrRowSource::Bound { source, .. }] => {
let alias = &source.alias;
let mut sql = format!(
"SELECT {}.\"id\" AS \"value\"\nFROM {} AS {}",
qi(alias),
source_ref(source),
qi(alias)
);
append_filter(&mut sql, &sel.filter);
sql
}
rows => match rows.first() {
Some(IrRowSource::Free(IrFreeExpr::Scalar(e))) => format!("SELECT {} AS \"value\"", emit_expr(e)),
_ => "SELECT NULL AS \"value\"".to_string(),
},
},
IrStmt::PathSelect(sel) => {
let from_sql = emit_path_joins(&sel.root, &sel.joins);
let scalar_expr = match &sel.result {
IrPathResult::Scalar(e, _) => emit_expr(e),
IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
};
let mut sql = format!("SELECT {} AS \"value\"\nFROM {}", scalar_expr, from_sql);
append_filter(&mut sql, &sel.filter);
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
sql
}
_ => "SELECT NULL AS \"value\"".to_string(),
}
}
fn emit_global_cte_parts(global_ctes: &[IrGlobalCte]) -> Vec<String> {
global_ctes
.iter()
.map(|g| match g {
IrGlobalCte::Session(s) => format!(
"\"{}\" AS (SELECT ${}::{} AS \"value\")",
s.cte_name,
s.param_index + 1,
s.pg_type
),
IrGlobalCte::Computed(c) => {
let body = emit_for_global_cte(&c.stmt);
format!("\"{}\" AS (\n{}\n)", c.cte_name, body)
}
})
.collect()
}
pub fn emit(ir: &IrOutput) -> SqlOutput {
let out = with_subtype_fanouts(&ir.subtype_fanouts, || {
with_correlated_ctes(&ir.ctes, || emit_output(ir))
});
#[cfg(debug_assertions)]
if let Some(problem) = forward_cte_reference(&out.sql) {
panic!("{problem}\n{}", out.sql);
}
out
}
#[cfg(debug_assertions)]
fn forward_cte_reference(sql: &str) -> Option<String> {
let definitions: Vec<(String, usize)> = sql
.match_indices("\" AS (")
.filter_map(|(end, _)| {
let head = &sql[..end];
let start = head.rfind('"')?;
Some((head[start + 1..].to_string(), start))
})
.collect();
for (name, defined_at) in &definitions {
let quoted = qi(name);
for prefix in ["FROM ", "JOIN ", "CROSS JOIN "] {
let needle = format!("{prefix}{quoted}");
if let Some(used_at) = sql.find(&needle)
&& used_at < *defined_at
{
return Some(format!(
"CTE {quoted} is read at byte {used_at} but not defined until {defined_at}"
));
}
}
}
None
}
fn emit_output(ir: &IrOutput) -> SqlOutput {
let mut out = match &ir.stmt {
IrStmt::Update(upd) => emit_update_stmt(upd, &ir.ctes),
IrStmt::For(f) => emit_for_stmt(f, &ir.ctes),
stmt => {
let mut o = match stmt {
IrStmt::Select(sel) => emit_select_stmt(sel, &ir.ctes),
IrStmt::PathSelect(sel) => emit_path_select(sel),
IrStmt::Insert(ins) => emit_insert_stmt(ins),
IrStmt::Delete(del) => emit_delete_stmt(del),
IrStmt::Group(grp) => emit_group(grp),
IrStmt::FunctionSelect(sel) => emit_function_select(sel),
IrStmt::VectorSearch(vs) => emit_vector_search(vs),
IrStmt::FtsSearch(fs) => emit_fts_search(fs),
IrStmt::ScalarUnion(branches) => SqlOutput {
sql: emit_scalar_union(branches),
shape: ShapeDescriptor {
root: ShapeNode::Scalar {
name: String::new(),
position: 0,
},
},
inference_plan: None,
},
IrStmt::Update(_) | IrStmt::For(_) => unreachable!(),
};
if !ir.ctes.is_empty() {
o.sql = merge_into_existing_with(&o.sql, &emit_user_cte_parts(&ir.ctes))
.unwrap_or_else(|| format!("{}{}", emit_cte_prefix(&ir.ctes), o.sql));
}
o
}
};
if !ir.global_ctes.is_empty() {
let global_parts = emit_global_cte_parts(&ir.global_ctes);
out.sql = merge_into_existing_with(&out.sql, &global_parts)
.unwrap_or_else(|| format!("WITH {}\n{}", global_parts.join(",\n "), out.sql));
}
out
}
fn unwrap_unnest_for_any(right: &IrExpr, rendered: &str) -> String {
if let Some(array) = unnest_argument(right) {
return array;
}
if let IrExpr::TypeCast(cast) = right
&& let Some(array) = unnest_argument(&cast.expr)
{
return format!("({})::{}[]", array, cast.pg_type);
}
rendered.to_string()
}
fn unnest_argument(expr: &IrExpr) -> Option<String> {
let IrExpr::FunctionCall(call) = expr else {
return None;
};
(call.schema.is_none() && call.name == "unnest" && call.sql_template.is_none() && call.args.len() == 1)
.then(|| emit_expr(&call.args[0]))
}
fn merge_into_existing_with(sql: &str, parts: &[String]) -> Option<String> {
if parts.is_empty() || !(sql.starts_with("WITH\n") || sql.starts_with("WITH ")) {
return None;
}
Some(format!("WITH {},\n {}", parts.join(",\n "), &sql[5..]))
}
fn qi(s: &str) -> String {
format!("\"{}\"", s.replace('"', "\"\""))
}
fn pg_schema(module: &str) -> String {
if module == "default" {
"\"public\"".into()
} else {
qi(module)
}
}
pub fn pg_schema_str(module: &str) -> String {
pg_schema(module)
}
fn qn(module: &str, name: &str) -> String {
format!("{}.{}", pg_schema(module), qi(name))
}
fn sql_str(s: &str) -> String {
format!("'{}'", s.replace('\'', "''"))
}
fn type_disc(type_name: &str) -> String {
format!("{}::text", sql_str(type_name))
}
fn module_of(type_name: &str) -> &str {
type_name.split("::").next().unwrap_or("public")
}
fn source_ref(src: &IrSource) -> String {
if let Some(cte_name) = src.table.strip_prefix("@cte:") {
return match correlated_cte_iterator(cte_name) {
Some(iterator) => format!(
"(SELECT * FROM {} WHERE {}.{} = {}.\"v\")",
qi(cte_name),
qi(cte_name),
qi(OUTER_KEY),
qi(&iterator),
),
None => qi(cte_name),
};
}
if let Some(row) = src.table.strip_prefix("@row:") {
return format!("(SELECT ({row}).*)");
}
match source_fanout(src) {
Some(fanout) => format!("(\n{}\n)", emit_poly_union(&fanout.implementors, &fanout.columns)),
None => junction_ref(module_of(&src.type_name), &src.table),
}
}
fn target_ref(src: &IrSource) -> String {
match src.table.strip_prefix("@cte:") {
Some(cte_name) => qi(cte_name),
None => qn(module_of(&src.type_name), &src.table),
}
}
thread_local! {
static SUBTYPE_FANOUTS: std::cell::RefCell<HashMap<(String, String), IrPolyFanout>> =
std::cell::RefCell::new(HashMap::new());
static CORRELATED_CTES: std::cell::RefCell<HashMap<String, String>> =
std::cell::RefCell::new(HashMap::new());
}
fn correlated_cte_iterator(cte_name: &str) -> Option<String> {
CORRELATED_CTES.with(|cell| cell.borrow().get(cte_name).cloned())
}
fn with_correlated_ctes<T>(ctes: &[IrCteDef], emit: impl FnOnce() -> T) -> T {
fn collect(ctes: &[IrCteDef], into: &mut HashMap<String, String>) {
for c in ctes {
if let Some(iterator) = &c.correlated_to {
into.insert(c.name.clone(), iterator.clone());
}
match &c.stmt {
IrStmt::For(f) => collect(&f.body_ctes, into),
IrStmt::Insert(ins) => collect(&ins.nested_ctes, into),
IrStmt::Update(upd) => collect(&upd.nested_ctes, into),
_ => {}
}
}
}
let mut collected = HashMap::new();
collect(ctes, &mut collected);
let previous = CORRELATED_CTES.with(|cell| cell.replace(collected));
let result = emit();
CORRELATED_CTES.with(|cell| cell.replace(previous));
result
}
fn with_subtype_fanouts<T>(fanouts: &HashMap<(String, String), IrPolyFanout>, emit: impl FnOnce() -> T) -> T {
let previous = SUBTYPE_FANOUTS.with(|cell| cell.replace(fanouts.clone()));
let result = emit();
SUBTYPE_FANOUTS.with(|cell| cell.replace(previous));
result
}
fn source_fanout(src: &IrSource) -> Option<IrPolyFanout> {
if let Some(fanout) = &src.poly {
return Some(fanout.clone());
}
if src.table.starts_with('@') {
return None;
}
let key = (module_of(&src.type_name).to_string(), src.table.clone());
SUBTYPE_FANOUTS.with(|cell| cell.borrow().get(&key).cloned())
}
pub const DML_CTE: &str = "_dml";
fn junction_ref(module: &str, junction_table: &str) -> String {
if let Some(cte_name) = junction_table.strip_prefix("@cte:") {
return qi(cte_name);
}
let Some((tables, columns)) = crate::ir::parse_inherited_junction(junction_table) else {
return qn(module, junction_table);
};
let columns = columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
let branches = tables
.iter()
.map(|(module, table)| format!("SELECT {columns} FROM {}", qn(module, table)))
.collect::<Vec<_>>()
.join(" UNION ALL ");
format!("({branches})")
}
fn result_type_disc(path: &IrPathSelect, alias: &str, type_name: &str) -> String {
match terminal_source(path, alias) {
Some(source) => source_type_disc(source),
None => type_disc(type_name),
}
}
fn terminal_source<'a>(path: &'a IrPathSelect, alias: &str) -> Option<&'a IrSource> {
if path.root.alias == alias {
return Some(&path.root);
}
path.joins
.iter()
.map(path_join_target)
.find(|target| target.alias == alias)
}
fn path_join_target(join: &IrPathJoin) -> &IrSource {
match join {
IrPathJoin::Single { target, .. }
| IrPathJoin::Multi { target, .. }
| IrPathJoin::BacklinkSingle { target, .. }
| IrPathJoin::BacklinkMulti { target, .. }
| IrPathJoin::Function { target, .. }
| IrPathJoin::Lateral { target, .. } => target,
}
}
fn source_type_disc(src: &IrSource) -> String {
match source_fanout(src) {
Some(_) => format!("{}.\"__type__\"", qi(&src.alias)),
None => type_disc(&src.type_name),
}
}
fn emit_poly_union(implementors: &[IrPolyImplementor], columns: &[String]) -> String {
let col_list = columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
implementors
.iter()
.map(|imp| {
format!(
" SELECT {}::text AS \"__type__\", {} FROM {}",
sql_str(&imp.type_name),
col_list,
qn(&imp.module, &imp.table),
)
})
.collect::<Vec<_>>()
.join("\n UNION ALL\n")
}
fn emit_select_stmt(sel: &IrSelect, ctes: &[IrCteDef]) -> SqlOutput {
match sel.rows.as_slice() {
[IrRowSource::Bound { source, shape }] => emit_bound_select(sel, source, shape),
rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
emit_bound_union_select(sel, rows)
}
rows => emit_free_rows(sel, rows, ctes),
}
}
fn bound_union_from_clause(rows: &[IrRowSource], common_columns: &[String]) -> String {
let columns = common_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
rows.iter()
.map(|row| match row {
IrRowSource::Bound { source, .. } if !common_columns.is_empty() => {
let own_type = match source_fanout(source) {
Some(_) => "\"__type__\"".to_string(),
None => format!("{} AS \"__type__\"", type_disc(&source.type_name)),
};
format!(" SELECT {own_type}, {columns} FROM {}", source_ref(source))
}
IrRowSource::Bound { source, .. } => format!(" SELECT * FROM {}", source_ref(source)),
IrRowSource::Free(_) => unreachable!("caller checked every row is bound"),
})
.collect::<Vec<_>>()
.join("\n UNION ALL\n")
}
fn emit_bound_union_select(sel: &IrSelect, rows: &[IrRowSource]) -> SqlOutput {
let IrRowSource::Bound { source, shape } = &rows[0] else {
unreachable!("caller checked every row is bound")
};
let alias = &source.alias;
let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
let row_type = if sel.poly_columns.is_empty() {
type_disc(&source.type_name)
} else {
format!("{}.\"__type__\"", qi(alias))
};
let mut parts = vec![row_type];
parts.extend(pointer_exprs);
let mut sql = format!(
"SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
if sel.distinct { "DISTINCT " } else { "" },
parts.join(",\n "),
bound_union_from_clause(rows, &sel.poly_columns),
qi(alias),
);
append_filter(&mut sql, &sel.filter);
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
SqlOutput {
sql,
shape: ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: Some(source.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(shape_pointers),
has_implicit_id: shape_has_implicit_id(shape),
},
},
inference_plan: None,
}
}
fn emit_bound_select(sel: &IrSelect, source: &IrSource, shape: &[IrShapePointer]) -> SqlOutput {
let alias = &source.alias;
let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
let type_expr = if sel.polymorphic {
format!("{}.\"__type__\"", qi(alias))
} else {
type_disc(&source.type_name)
};
let mut parts = vec![type_expr];
parts.extend(pointer_exprs);
let tuple = parts.join(",\n ");
let distinct = if sel.distinct { "DISTINCT " } else { "" };
let from_clause = if let Some(dml) = &sel.dml_source {
let mut cte_parts = match dml.as_ref() {
IrStmt::Update(upd) if update_has_any_multilink(upd) => emit_update_multilink_ctes(upd, DML_CTE),
IrStmt::Insert(ins) if insert_has_any_multilink(ins) => emit_insert_multilink_ctes(ins, DML_CTE),
IrStmt::Insert(ins) if !ins.nested_ctes.is_empty() => {
let mut parts = emit_user_cte_parts(&ins.nested_ctes);
parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
parts
}
IrStmt::Update(upd) if !upd.nested_ctes.is_empty() => {
let mut parts = emit_user_cte_parts(&upd.nested_ctes);
parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
parts
}
IrStmt::Update(upd) if !upd.poly_implementors.is_empty() => emit_poly_update_dml_ctes(upd, "_dml"),
IrStmt::Delete(del) if !del.poly_implementors.is_empty() => emit_poly_delete_dml_ctes(del, "_dml"),
_ => vec![format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml))],
};
let (enqueue_v, enqueue_s) = match dml.as_ref() {
IrStmt::Insert(ins) => (ins.enqueue_vector.as_slice(), ins.enqueue_search.as_slice()),
IrStmt::Update(upd) => (upd.enqueue_vector.as_slice(), upd.enqueue_search.as_slice()),
_ => (&[][..], &[][..]),
};
cte_parts.extend(enqueue_ctes(enqueue_v, "_dml"));
cte_parts.extend(enqueue_search_ctes(enqueue_s, "_dml", enqueue_v.len()));
format!(
"WITH\n{}\nSELECT {}(\n {}\n) AS result\nFROM \"_dml\" AS {}",
cte_parts.join(",\n"),
distinct,
tuple,
qi(alias)
)
} else if sel.polymorphic && !source.table.starts_with("@cte:") {
let union_sql = emit_poly_union(&sel.poly_implementors, &sel.poly_columns);
format!(
"SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
distinct,
tuple,
union_sql,
qi(alias)
)
} else {
format!(
"SELECT {}(\n {}\n) AS result\nFROM {} AS {}",
distinct,
tuple,
source_ref(source),
qi(alias)
)
};
let mut sql = from_clause;
append_filter(&mut sql, &sel.filter);
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
append_lock_clause(&mut sql, &sel.lock);
let root_pointers = prepend_type(shape_pointers);
SqlOutput {
sql,
shape: ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: Some(source.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: root_pointers,
has_implicit_id: shape_has_implicit_id(shape),
},
},
inference_plan: None,
}
}
fn nested_cte_from(ctes: &[IrCteDef], reading: &str, lead: &str) -> String {
let joined: Vec<String> = ctes
.iter()
.map(|c| qi(&c.name))
.filter(|name| reading.contains(&format!("{name}.")))
.collect();
if joined.is_empty() {
return String::new();
}
format!("{lead}FROM {}", joined.join(", "))
}
fn emit_scalar_union(branches: &[IrStmt]) -> String {
let operands: Vec<String> = branches
.iter()
.enumerate()
.map(|(i, branch)| {
format!(
"SELECT \"v\" FROM (\n{}\n) AS \"_u{i}\"",
emit_dml_as_cte_source(branch)
)
})
.collect();
format!(
"SELECT ROW(v) AS result, v FROM (\n{}\n) AS _scalar",
operands.join("\nUNION ALL\n")
)
}
fn emit_dml_as_cte_source(stmt: &IrStmt) -> String {
match stmt {
IrStmt::ScalarUnion(branches) => format!(" {}", emit_scalar_union(branches)),
IrStmt::Insert(ins) => {
let rewrite_cols: std::collections::HashSet<&str> =
ins.rewrites.iter().map(|r| r.column.as_str()).collect();
let cols: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(c, _)| format!(" {}", qi(c)))
.chain(ins.rewrites.iter().map(|r| format!(" {}", qi(&r.column))))
.collect();
let vals: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(_, e)| format!(" {}", emit_expr(e)))
.chain(ins.rewrites.iter().map(|r| format!(" {}", emit_expr(&r.expr))))
.collect();
let mut sql = if !ins.nested_ctes.is_empty() || ins.guard.is_some() {
format!(" {}", emit_insert_row_sql(ins))
} else if cols.is_empty() {
format!(" INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
} else {
format!(
" INSERT INTO {} (\n{}\n ) VALUES (\n{}\n )",
target_ref(&ins.target),
cols.join(",\n"),
vals.join(",\n"),
)
};
if let Some(conflict) = &ins.unless_conflict {
emit_conflict(&mut sql, conflict);
}
sql.push_str("\n RETURNING *");
sql
}
IrStmt::Update(upd) => {
let alias = &upd.target.alias;
let sets = update_set_fragments(&upd.assignments, &upd.rewrites, " ");
let mut sql = format!(
" UPDATE {} AS {}\n SET\n{}",
target_ref(&upd.target),
qi(alias),
sets.join(",\n"),
);
sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
append_filter(&mut sql, &upd.filter);
sql.push_str(&format!("\n RETURNING {}.*", qi(alias)));
sql
}
IrStmt::Delete(del) => {
let alias = &del.target.alias;
let mut sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
append_filter(&mut sql, &del.filter);
sql.push_str("\n RETURNING *");
sql
}
IrStmt::Select(inner) => match inner.rows.as_slice() {
[IrRowSource::Bound { source, .. }]
if let Some(read) = inner.dml_source.as_deref()
&& !matches!(read, IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::Delete(_)) =>
{
let mut sql = format!(
" SELECT {}* FROM (\n{}\n ) AS {}",
if inner.distinct { "DISTINCT " } else { "" },
emit_dml_as_cte_source(read),
qi(&source.alias),
);
append_filter(&mut sql, &inner.filter);
append_order_by(&mut sql, &inner.order_by);
append_offset_limit(&mut sql, &inner.offset, &inner.limit);
sql
}
[IrRowSource::Bound { source, .. }] => {
let from = if inner.polymorphic && !source.table.starts_with("@cte:") {
format!(
"(\n{}\n ) AS {}",
emit_poly_union(&inner.poly_implementors, &inner.poly_columns),
qi(&source.alias),
)
} else {
format!("{} AS {}", source_ref(source), qi(&source.alias))
};
let mut sql = format!(
" SELECT {}* FROM {}",
if inner.distinct { "DISTINCT " } else { "" },
from
);
append_filter(&mut sql, &inner.filter);
append_order_by(&mut sql, &inner.order_by);
append_offset_limit(&mut sql, &inner.offset, &inner.limit);
sql
}
rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
let IrRowSource::Bound { source, .. } = &rows[0] else {
unreachable!("checked by the guard")
};
let mut sql = format!(
" SELECT {}* FROM (\n{}\n ) AS {}",
if inner.distinct { "DISTINCT " } else { "" },
bound_union_from_clause(rows, &inner.poly_columns),
qi(&source.alias),
);
append_filter(&mut sql, &inner.filter);
append_order_by(&mut sql, &inner.order_by);
append_offset_limit(&mut sql, &inner.offset, &inner.limit);
sql
}
_ => emit_select_stmt(inner, &[]).sql,
},
IrStmt::FunctionSelect(sel) => {
let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
let mut sql = format!(" SELECT * FROM {} AS {}", fn_call, qi(&sel.alias));
append_filter(&mut sql, &sel.filter);
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
sql
}
IrStmt::For(f) => {
if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
return " SELECT NULL AS \"id\" WHERE FALSE".to_string();
}
let (values_from, _) = emit_for_iterator(&f.iterator, &format!("_for_{}", f.var_name));
let body =
prefix_body_ctes(emit_dml_as_cte_source(&f.body), &uncorrelated_body_ctes(f)).replace('\n', "\n ");
format!(
" SELECT \"_body\".*\n FROM {}\n CROSS JOIN LATERAL (\n {}\n ) AS \"_body\"",
values_from.replace('\n', "\n "),
body,
)
}
IrStmt::Group(grp) if matches!(grp.output, IrGroupOutput::Elements) => emit_group_elements_source(grp),
IrStmt::Group(_) | IrStmt::VectorSearch(_) | IrStmt::FtsSearch(_) => {
unreachable!("cannot appear as a CTE source")
}
IrStmt::PathSelect(ps) => {
let distinct = if ps.distinct { "DISTINCT " } else { "" };
let projection = match &ps.result {
IrPathResult::Scalar(e, _) => format!("{}{} AS v", distinct, emit_expr(e)),
IrPathResult::Object { alias, .. } => format!("{}{}.*", distinct, qi(alias)),
};
let mut sql = format!(
"SELECT {}\n FROM {}",
projection,
emit_path_joins(&ps.root, &ps.joins)
);
append_filter(&mut sql, &ps.filter);
append_order_by(&mut sql, &ps.order_by);
append_offset_limit(&mut sql, &ps.offset, &ps.limit);
match &ps.result {
IrPathResult::Scalar(e, _) => {
let row_value = if enum_type_of_expr(e).is_some() { "v::text" } else { "v" };
format!(" SELECT ROW({row_value}) AS result, v FROM ({sql}) AS _scalar")
}
IrPathResult::Object { .. } => format!(" {sql}"),
}
}
}
}
fn emit_poly_update_dml_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
let alias = &upd.target.alias;
let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
let mut cte_parts = vec![];
let mut union_parts = vec![];
for (i, imp) in upd.poly_implementors.iter().enumerate() {
let cte_name = format!("{}__u{}", name, i);
let mut upd_sql = format!(
"UPDATE {} AS {}\nSET {}",
qn(&imp.module, &imp.table),
qi(alias),
sets.join(", "),
);
append_filter(&mut upd_sql, &upd.filter);
upd_sql.push_str(&format!("\nRETURNING {}", col_list));
cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
union_parts.push(format!(
"SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
sql_str(&imp.type_name),
col_list,
cte_name,
));
}
cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
cte_parts
}
fn concrete_poly_update(upd: &IrUpdate, imp: &IrPolyImplementor) -> IrUpdate {
let interface_prefix = format!("{}.", upd.target.table);
let own_prefix = format!("{}.", imp.table);
let own_junction = |table: &mut String, module: &mut String| {
if let Some(link) = table.strip_prefix(&interface_prefix) {
*table = format!("{own_prefix}{link}");
module.clone_from(&imp.module);
}
};
let mut concrete = upd.clone();
concrete.poly_implementors = vec![];
concrete.target.table = imp.table.clone();
concrete.target.type_name = imp.type_name.clone();
concrete.target.poly = None;
concrete
.multi_link_clears
.iter_mut()
.for_each(|c| own_junction(&mut c.junction_table, &mut c.module));
concrete
.multi_link_replaces
.iter_mut()
.chain(concrete.multi_link_appends.iter_mut())
.chain(concrete.multi_link_removals.iter_mut())
.for_each(|m| own_junction(&mut m.junction_table, &mut m.module));
concrete
}
fn emit_poly_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
let mut cte_parts = emit_user_cte_parts(&upd.nested_ctes);
let mut union_parts = vec![];
for (i, imp) in upd.poly_implementors.iter().enumerate() {
let cte_name = format!("{}__u{}", name, i);
cte_parts.extend(emit_update_multilink_cte_parts(
&concrete_poly_update(upd, imp),
&cte_name,
false,
));
union_parts.push(format!(
"SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
sql_str(&imp.type_name),
col_list,
cte_name,
));
}
cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
cte_parts
}
fn emit_poly_delete_dml_ctes(del: &IrDelete, name: &str) -> Vec<String> {
let alias = &del.target.alias;
let col_list = del.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
let mut cte_parts = vec![];
let mut union_parts = vec![];
for (i, imp) in del.poly_implementors.iter().enumerate() {
let cte_name = format!("{}__d{}", name, i);
let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
append_filter(&mut del_sql, &del.filter);
del_sql.push_str(&format!("\nRETURNING {}", col_list));
cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
union_parts.push(format!(
"SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
sql_str(&imp.type_name),
col_list,
cte_name,
));
}
cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
cte_parts
}
fn update_has_any_multilink(upd: &IrUpdate) -> bool {
!upd.multi_link_clears.is_empty()
|| !upd.multi_link_replaces.is_empty()
|| !upd.multi_link_appends.is_empty()
|| !upd.multi_link_removals.is_empty()
}
fn ml_clear_exclusion(rep: Option<&IrMultiLinkMutation>) -> String {
match rep {
Some(rep) => {
let vals_ref = emit_multilink_values_subquery(&rep.values, &[]);
format!(
" AND {} NOT IN (SELECT \"_v\".\"id\" FROM {} AS \"_v\")",
qi(&rep.target_col),
vals_ref
)
}
None => String::new(),
}
}
fn emit_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
emit_update_multilink_cte_parts(upd, name, true)
}
fn emit_update_multilink_cte_parts(upd: &IrUpdate, name: &str, emit_nested: bool) -> Vec<String> {
let alias = &upd.target.alias;
let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
let ids_name = format!("{}__ids", name);
let mut parts: Vec<String> = if emit_nested {
emit_user_cte_parts(&upd.nested_ctes)
} else {
vec![]
};
if has_scalar_changes {
let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
let mut upd_sql = format!(
"UPDATE {} AS {}\nSET {}",
target_ref(&upd.target),
qi(alias),
sets.join(", "),
);
upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
append_filter(&mut upd_sql, &upd.filter);
upd_sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, upd_sql));
} else {
let mut sel = format!(
"SELECT {}.* FROM {} AS {}",
qi(alias),
target_ref(&upd.target),
qi(alias),
);
append_filter(&mut sel, &upd.filter);
parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sel));
}
for (i, clr) in upd.multi_link_clears.iter().enumerate() {
let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
let del = format!(
"DELETE FROM {} WHERE {} IN (SELECT id FROM \"{}\"){}",
qn(&clr.module, &clr.junction_table),
qi(&clr.source_col),
ids_name,
exclude,
);
parts.push(format!("\"{}__clr_{}\" AS (\n{}\n)", name, i, del));
}
for (i, app) in upd.multi_link_appends.iter().enumerate() {
parts.push(emit_ml_append_cte(
app,
&ids_name,
&upd.target.alias,
&format!("{}__ml_add_{}", name, i),
));
}
for (i, rem) in upd.multi_link_removals.iter().enumerate() {
parts.push(emit_ml_remove_cte(
rem,
&ids_name,
&upd.target.alias,
&format!("{}__ml_rm_{}", name, i),
));
}
for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
parts.push(emit_ml_append_cte(
rep,
&ids_name,
&upd.target.alias,
&format!("{}__ml_rep_{}", name, i),
));
}
parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
parts
}
fn insert_has_any_multilink(ins: &IrInsert) -> bool {
!ins.multi_link_appends.is_empty()
}
fn emit_insert_multilink_ctes(ins: &IrInsert, name: &str) -> Vec<String> {
let ids_name = format!("{}__ids", name);
let mut parts: Vec<String> = emit_user_cte_parts(&ins.nested_ctes);
let mut insert_sql = emit_insert_row_sql(ins);
if let Some(conflict) = &ins.unless_conflict {
emit_conflict(&mut insert_sql, conflict);
}
insert_sql.push_str("\nRETURNING *");
parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, insert_sql));
for (i, app) in ins.multi_link_appends.iter().enumerate() {
parts.push(emit_ml_append_cte(
app,
&ids_name,
&ins.target.alias,
&format!("{}__ml_add_{}", name, i),
));
}
parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
parts
}
fn emit_for_dml_ctes_within(f: &IrFor, name: &str, outer_alias: Option<&str>) -> Vec<String> {
let iter_alias = format!("_for_{}", f.var_name);
let (_, plain_iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
let correlated = outer_alias;
let iter_cte = match correlated {
Some(outer) => emit_nested_for_iterator(&f.iterator, &iter_alias, outer),
None => plain_iter_cte,
};
let mut parts: Vec<String> = vec![iter_cte];
parts.extend(emit_user_cte_parts(&f.body_ctes));
let ids_name = format!("{}__ids", name);
if correlated.is_some()
&& let IrStmt::Insert(ins) = f.body.as_ref()
&& ins.multi_link_appends.is_empty()
{
let rows_name = format!("{}__rows", name);
let outer_source = format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY));
parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, rows_name));
return parts;
}
match f.body.as_ref() {
IrStmt::Insert(ins) => {
let rewrite_cols: std::collections::HashSet<&str> =
ins.rewrites.iter().map(|r| r.column.as_str()).collect();
let cols: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(c, _)| qi(c))
.chain(ins.rewrites.iter().map(|r| qi(&r.column)))
.collect();
let values: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(_, e)| emit_expr(e))
.chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
.collect();
let rows_name = format!("{}__rows", name);
let correlated_targets =
correlated_append_indices(&ins.multi_link_appends, &[&f.body_ctes, &ins.nested_ctes]);
if correlated_targets.is_empty() {
let mut sql = format!(
"INSERT INTO {} ({})\nSELECT {} FROM {}",
target_ref(&ins.target),
cols.join(", "),
values.join(", "),
qi(&iter_alias),
);
if let Some(conflict) = &ins.unless_conflict {
emit_conflict(&mut sql, conflict);
}
sql.push_str("\nRETURNING *");
parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
} else {
let outer_source = match correlated {
Some(_) => format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY)),
None => format!("{}.{}", qi(&iter_alias), qi("v")),
};
parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
}
for (i, append) in ins.multi_link_appends.iter().enumerate() {
let cte_name = format!("{}__ml_add_{}", name, i);
if correlated_targets.contains(&i) {
let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
unreachable!("only a CTE-backed value can carry an iteration key")
};
parts.push(emit_correlated_ml_append_cte(append, &rows_name, target, &cte_name));
} else {
parts.push(emit_ml_append_cte(append, &ids_name, &ins.target.alias, &cte_name));
}
}
}
IrStmt::Update(upd) => {
let alias = &upd.target.alias;
if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
const ITER_COL: &str = "_iter";
let generated: Vec<Option<&IrInsert>> = upd
.multi_link_appends
.iter()
.map(|a| per_iteration_insert(a, &upd.nested_ctes))
.collect();
let new_cols: String = generated
.iter()
.enumerate()
.filter_map(|(i, ins)| {
ins.map(|ins| {
format!(
", {} AS {}",
ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
qi(&format!("_new_{i}"))
)
})
})
.collect();
let mut ids_sql = format!(
"\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
ids_name,
qi(alias),
qi(&iter_alias),
qi(ITER_COL),
new_cols,
target_ref(&upd.target),
qi(alias),
qi(&iter_alias),
);
append_filter(&mut ids_sql, &upd.filter);
ids_sql.push_str("\n)");
parts.push(ids_sql);
for (i, append) in upd.multi_link_appends.iter().enumerate() {
match generated[i] {
Some(ins) => {
parts.push(emit_for_nested_insert_cte(
ins,
&ids_name,
&iter_alias,
&format!("{}__ml_ins_{}", name, i),
&format!("_new_{i}"),
));
parts.push(emit_for_ml_append_cte(
append,
&ids_name,
&format!("{}__ml_add_{}", name, i),
&format!("_new_{i}"),
));
}
None => parts.push(emit_for_ml_append_cte(
append,
&ids_name,
&format!("{}__ml_add_{}", name, i),
ITER_COL,
)),
}
}
} else {
let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
let mut sql = format!(
"UPDATE {} AS {}\nSET {}\nFROM {}",
target_ref(&upd.target),
qi(alias),
sets.join(", "),
qi(&iter_alias),
);
append_filter(&mut sql, &upd.filter);
sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
}
}
IrStmt::For(inner) => {
let inner_alias = format!("_for_{}", inner.var_name);
parts.push(emit_nested_for_iterator(&inner.iterator, &inner_alias, &iter_alias));
parts.extend(emit_user_cte_parts(&inner.body_ctes));
let IrStmt::Insert(ins) = inner.body.as_ref() else {
unreachable!("a nested for-loop's own body is an insert: {:?}", inner.body)
};
let rewrite_cols: std::collections::HashSet<&str> =
ins.rewrites.iter().map(|r| r.column.as_str()).collect();
let cols: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(c, _)| qi(c))
.chain(ins.rewrites.iter().map(|r| qi(&r.column)))
.collect();
let values: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(_, e)| emit_expr(e))
.chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
.collect();
let mut sql = format!(
"INSERT INTO {} ({})\nSELECT {} FROM {}",
target_ref(&ins.target),
cols.join(", "),
values.join(", "),
nested_for_from(&inner_alias, &iter_alias),
);
if let Some(conflict) = &ins.unless_conflict {
emit_conflict(&mut sql, conflict);
}
sql.push_str("\nRETURNING *");
parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
for (i, append) in ins.multi_link_appends.iter().enumerate() {
parts.push(emit_ml_append_cte(
append,
&ids_name,
&ins.target.alias,
&format!("{}__ml_add_{}", name, i),
));
}
}
other => unreachable!("for-loop body is not a mutation: {other:?}"),
}
parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
parts
}
fn emit_user_cte_parts(ctes: &[IrCteDef]) -> Vec<String> {
let mut parts: Vec<String> = vec![];
for c in ctes {
let nested: &[IrCteDef] = match &c.stmt {
IrStmt::Insert(ins) if insert_has_any_multilink(ins) => &[],
IrStmt::Update(upd) if update_has_any_multilink(upd) => &[],
IrStmt::Insert(ins) => &ins.nested_ctes,
IrStmt::Update(upd) => &upd.nested_ctes,
_ => &[],
};
if !nested.is_empty() {
parts.extend(emit_user_cte_parts(nested));
}
if let IrStmt::Update(upd) = &c.stmt {
if !upd.poly_implementors.is_empty() {
parts.extend(if update_has_any_multilink(upd) {
emit_poly_update_multilink_ctes(upd, &c.name)
} else {
emit_poly_update_dml_ctes(upd, &c.name)
});
continue;
}
if update_has_any_multilink(upd) {
parts.extend(emit_update_multilink_ctes(upd, &c.name));
continue;
}
}
if let IrStmt::Insert(ins) = &c.stmt
&& insert_has_any_multilink(ins)
{
parts.extend(emit_insert_multilink_ctes(ins, &c.name));
continue;
}
if let IrStmt::Delete(del) = &c.stmt
&& !del.poly_implementors.is_empty()
{
parts.extend(emit_poly_delete_dml_ctes(del, &c.name));
continue;
}
if let IrStmt::For(f) = &c.stmt
&& matches!(f.body.as_ref(), IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::For(_))
{
parts.extend(emit_for_dml_ctes_within(f, &c.name, c.correlated_to.as_deref()));
continue;
}
let body = emit_dml_as_cte_source(&c.stmt);
parts.push(match &c.correlated_to {
Some(iterator) => format!(
"\"{}\" AS (\nSELECT {}.\"v\" AS {}, \"_row\".*\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_row\"\n)",
c.name,
qi(iterator),
qi(OUTER_KEY),
qi(iterator),
body,
),
None => format!("\"{}\" AS (\n{}\n)", c.name, body),
});
}
parts
}
fn emit_cte_prefix(ctes: &[IrCteDef]) -> String {
format!("WITH\n{}\n", emit_user_cte_parts(ctes).join(",\n"))
}
pub(crate) fn collect_link_prop_names(vals: &IrMultiLinkValues, names: &mut Vec<String>) {
for (name, _) in &vals.link_props {
if !names.contains(name) {
names.push(name.clone());
}
}
match &vals.source {
IrMultiLinkValueSource::Union(a, b) => {
collect_link_prop_names(a, names);
collect_link_prop_names(b, names);
}
IrMultiLinkValueSource::Asserted { inner, .. } => collect_link_prop_names(inner, names),
_ => {}
}
}
fn emit_link_prop_cols(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
prop_names
.iter()
.map(|name| match vals.link_props.iter().find(|(n, _)| n == name) {
Some((_, expr)) => format!(", {} AS {}", emit_expr(expr), qi(name)),
None => format!(", NULL AS {}", qi(name)),
})
.collect()
}
fn emit_multilink_values_subquery(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
emit_multilink_values_inner(vals, prop_names, true)
}
fn emit_multilink_values_inner(vals: &IrMultiLinkValues, prop_names: &[String], bare_cte_ok: bool) -> String {
if let IrMultiLinkValueSource::Union(a, b) = &vals.source {
return format!(
"({}\nUNION ALL\n{})",
emit_multilink_values_inner(a, prop_names, false),
emit_multilink_values_inner(b, prop_names, false),
);
}
if let IrMultiLinkValueSource::Asserted {
fn_name,
inner,
message,
} = &vals.source
{
let inner_sql = emit_multilink_values_inner(inner, &[], false);
let prop_cols = emit_link_prop_cols(vals, prop_names);
return format!(
"(SELECT unnest(\"_pylon\".{}(ARRAY(SELECT \"_v\".\"id\" FROM {} AS \"_v\"){})) AS \"id\"{})",
qi(fn_name),
inner_sql,
assert_message_arg(message),
prop_cols,
);
}
let prop_cols = emit_link_prop_cols(vals, prop_names);
match &vals.source {
IrMultiLinkValueSource::CteRef(name) => {
if prop_cols.is_empty() && bare_cte_ok {
format!("\"{}\"", name)
} else {
format!("(SELECT \"_s\".\"id\"{} FROM \"{}\" AS \"_s\")", prop_cols, name)
}
}
IrMultiLinkValueSource::Select(s) => {
let [IrRowSource::Bound { source, .. }] = s.rows.as_slice() else {
unreachable!("IrMultiLinkValueSource::Select is always schema-bound")
};
let alias = &source.alias;
let mut sql = format!(
"(SELECT {}.\"id\"{} FROM {} AS {}",
qi(alias),
prop_cols,
source_ref(source),
qi(alias)
);
append_filter(&mut sql, &s.filter);
sql.push(')');
sql
}
IrMultiLinkValueSource::PathSelect(ps) => {
let root_alias = &ps.root.alias;
let target_alias = match &ps.result {
IrPathResult::Object { alias, .. } => alias,
IrPathResult::Scalar(..) => root_alias,
};
let mut sql = format!(
"(SELECT {}.\"id\"{} FROM {} AS {}",
qi(target_alias),
prop_cols,
source_ref(&ps.root),
qi(root_alias)
);
for join in &ps.joins {
sql.push_str(&emit_path_join_sql(join));
}
append_filter(&mut sql, &ps.filter);
sql.push(')');
sql
}
IrMultiLinkValueSource::Function(fs) => {
let args = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
let mut sql = format!(
"(SELECT {}.\"id\"{} FROM {}.{}({}) AS {}",
qi(&fs.alias),
prop_cols,
pg_schema(&fs.fn_module),
qi(&fs.fn_name),
args,
qi(&fs.alias),
);
append_filter(&mut sql, &fs.filter);
sql.push(')');
sql
}
IrMultiLinkValueSource::Union(..) | IrMultiLinkValueSource::Asserted { .. } => {
unreachable!("handled above")
}
}
}
fn emit_path_join_sql(join: &IrPathJoin) -> String {
match join {
IrPathJoin::Lateral { inner, target } => {
let projected = match &inner.result {
IrPathResult::Object { alias, .. } => alias.clone(),
IrPathResult::Scalar(..) => unreachable!("a lateral step is only built for an object-valued computed"),
};
let mut body = format!(
"SELECT {}.* FROM {}",
qi(&projected),
emit_path_joins(&inner.root, &inner.joins)
);
append_filter(&mut body, &inner.filter);
append_order_by(&mut body, &inner.order_by);
append_offset_limit(&mut body, &inner.offset, &inner.limit);
format!(" JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias))
}
IrPathJoin::Function {
fn_module,
fn_name,
args,
target,
} => {
let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
format!(
" CROSS JOIN LATERAL {}.{}({}) AS {}",
pg_schema(fn_module),
qi(fn_name),
args_sql,
qi(&target.alias),
)
}
IrPathJoin::Single {
source_alias,
fk_col,
target,
} => {
format!(
" JOIN {} AS {} ON {}.\"id\" = {}.{}",
source_ref(target),
qi(&target.alias),
qi(&target.alias),
qi(source_alias),
qi(fk_col)
)
}
IrPathJoin::Multi {
source_alias,
junction_alias,
join: ml_join,
target,
} => {
let (jt_ref, src_col, tgt_col) = match ml_join {
IrMultiLinkJoin::Standard { junction_table, module } => (
junction_ref(module, junction_table),
"source".to_string(),
"target".to_string(),
),
IrMultiLinkJoin::Through {
junction_table,
module,
source_col,
target_col,
} => (
junction_ref(module, junction_table),
source_col.clone(),
target_col.clone(),
),
IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
unreachable!("a forward multi-link path step never uses a backlink join variant")
}
};
format!(
" JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.{} = {}.\"id\"",
jt_ref,
qi(junction_alias),
qi(junction_alias),
qi(&src_col),
qi(source_alias),
source_ref(target),
qi(&target.alias),
qi(junction_alias),
qi(&tgt_col),
qi(&target.alias),
)
}
IrPathJoin::BacklinkSingle {
source_alias,
fk_col,
target,
} => {
format!(
" JOIN {} AS {} ON {}.{} = {}.\"id\"",
source_ref(target),
qi(&target.alias),
qi(&target.alias),
qi(fk_col),
qi(source_alias),
)
}
IrPathJoin::BacklinkMulti {
source_alias,
junction_alias,
junction_table,
module,
owner_col,
current_col,
target,
} => {
format!(
" JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.\"id\" = {}.{}",
junction_ref(module, junction_table),
qi(junction_alias),
qi(junction_alias),
qi(current_col),
qi(source_alias),
source_ref(target),
qi(&target.alias),
qi(&target.alias),
qi(junction_alias),
qi(owner_col),
)
}
}
}
pub(crate) fn append_value_is_the_loop_variable(values: &IrMultiLinkValues, var: &str) -> bool {
let IrMultiLinkValueSource::Select(sel) = &values.source else {
return false;
};
let Some(IrExpr::BinOp(cmp)) = sel.filter.as_ref() else {
return false;
};
matches!(
(&cmp.left, &cmp.op, &cmp.right),
(
IrExpr::ColumnRef { column, .. },
crate::parse::ast::BinOpKind::Eq,
IrExpr::ForVar { name, .. },
) if column == "id" && name == var
) && values.link_props.is_empty()
}
pub(crate) fn per_iteration_insert<'c>(mutation: &IrMultiLinkMutation, nested: &'c [IrCteDef]) -> Option<&'c IrInsert> {
let IrMultiLinkValueSource::CteRef(name) = &mutation.values.source else {
return None;
};
if !mutation.values.link_props.is_empty() {
return None;
}
nested.iter().find(|c| &c.name == name).and_then(|c| match &c.stmt {
IrStmt::Insert(ins) if ins.id_default_sql.is_some() && ins.multi_link_appends.is_empty() => Some(ins),
_ => None,
})
}
fn emit_for_nested_insert_cte(
ins: &IrInsert,
ids_name: &str,
iter_alias: &str,
cte_name: &str,
id_col: &str,
) -> String {
let mut cols = vec![qi("id")];
let mut values = vec![format!("\"{}\".{}", ids_name, qi(id_col))];
for (col, expr) in &ins.assignments {
cols.push(qi(col));
values.push(emit_expr(expr));
}
format!(
"\"{}\" AS (\nINSERT INTO {} ({})\nSELECT {} FROM \"{}\", {}\nWHERE \"{}\".{} = {}.\"v\"\nRETURNING {}\n)",
cte_name,
target_ref(&ins.target),
cols.join(", "),
values.join(", "),
ids_name,
qi(iter_alias),
ids_name,
qi("_iter"),
qi(iter_alias),
qi("id"),
)
}
fn emit_for_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, cte_name: &str, iter_col: &str) -> String {
format!(
"\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"{}\".\"id\", \"{}\".{} FROM \"{}\"\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
cte_name,
qn(&mutation.module, &mutation.junction_table),
qi(&mutation.source_col),
qi(&mutation.target_col),
ids_name,
ids_name,
qi(iter_col),
ids_name,
qi(&mutation.source_col),
qi(&mutation.target_col),
)
}
fn emit_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
let mut prop_names = vec![];
collect_link_prop_names(&mutation.values, &mut prop_names);
let vals_ref = emit_multilink_values_subquery(&mutation.values, &prop_names);
let extra_cols: String = prop_names.iter().map(|n| format!(", {}", qi(n))).collect();
let extra_select: String = prop_names.iter().map(|n| format!(", \"_v\".{}", qi(n))).collect();
let conflict_clause = if mutation.single {
let mut sets = vec![format!(
"{} = EXCLUDED.{}",
qi(&mutation.target_col),
qi(&mutation.target_col)
)];
sets.extend(prop_names.iter().map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n))));
format!(
"ON CONFLICT ({}) DO UPDATE SET {}",
qi(&mutation.source_col),
sets.join(", ")
)
} else if prop_names.is_empty() {
"ON CONFLICT DO NOTHING".to_string()
} else {
let sets: Vec<String> = prop_names
.iter()
.map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n)))
.collect();
format!(
"ON CONFLICT ({}, {}) DO UPDATE SET {}",
qi(&mutation.source_col),
qi(&mutation.target_col),
sets.join(", "),
)
};
let lateral = if vals_ref.starts_with('(') { "LATERAL " } else { "" };
let ins = format!(
"INSERT INTO {} ({}, {}{})\nSELECT {}.\"id\", \"_v\".\"id\"{} FROM \"{}\" AS {} CROSS JOIN {}{} AS \"_v\"\n{}\nRETURNING {}, {}",
qn(&mutation.module, &mutation.junction_table),
qi(&mutation.source_col),
qi(&mutation.target_col),
extra_cols,
qi(ids_alias),
extra_select,
ids_name,
qi(ids_alias),
lateral,
vals_ref,
conflict_clause,
qi(&mutation.source_col),
qi(&mutation.target_col),
);
format!("\"{}\" AS (\n{}\n)", cte_name, ins)
}
fn emit_ml_remove_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
let vals_ref = emit_multilink_values_subquery(&mutation.values, &[]);
let del = format!(
"DELETE FROM {} AS \"_jn\"\nUSING \"{}\" AS {}\nWHERE \"_jn\".{} = {}.\"id\"\n AND \"_jn\".{} IN (SELECT \"id\" FROM {})\nRETURNING \"_jn\".{}, \"_jn\".{}",
qn(&mutation.module, &mutation.junction_table),
ids_name,
qi(ids_alias),
qi(&mutation.source_col),
qi(ids_alias),
qi(&mutation.target_col),
vals_ref,
qi(&mutation.source_col),
qi(&mutation.target_col),
);
format!("\"{}\" AS (\n{}\n)", cte_name, del)
}
use crate::ir::IrConflict;
fn emit_conflict(sql: &mut String, conflict: &IrConflict) {
let on_sql = conflict.on.as_ref().map(|e| match e {
IrExpr::Tuple(elements) => format!("({})", elements.iter().map(emit_expr).collect::<Vec<_>>().join(", ")),
other => format!("({})", emit_expr(other)),
});
match (&on_sql, &conflict.do_update) {
(None, None) => sql.push_str(" ON CONFLICT DO NOTHING"),
(Some(on), None) => sql.push_str(&format!(" ON CONFLICT {} DO NOTHING", on)),
(None, Some(updates)) => {
sql.push_str(&format!(" ON CONFLICT DO UPDATE SET {}", do_update_sets(updates)));
}
(Some(on), Some(updates)) => {
sql.push_str(&format!(
" ON CONFLICT {} DO UPDATE SET {}",
on,
do_update_sets(updates),
));
}
}
if conflict.do_update.is_some()
&& let Some(predicate) = &conflict.do_update_where
{
sql.push_str(&format!(" WHERE {}", emit_expr(predicate)));
}
}
fn do_update_sets(updates: &[(String, IrExpr)]) -> String {
updates
.iter()
.map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
.collect::<Vec<_>>()
.join(", ")
}
fn is_integer_expr(expr: &IrExpr) -> bool {
match expr {
IrExpr::ColumnRef { pg_type, .. } => matches!(
pg_type.as_str(),
"int2" | "int4" | "int8" | "integer" | "bigint" | "smallint"
),
IrExpr::Literal(crate::ir::IrLiteral::Int(_)) => true,
IrExpr::BinOp(op) => is_integer_expr(&op.left) && is_integer_expr(&op.right),
_ => false,
}
}
fn is_raw_scalar(expr: &IrExpr) -> bool {
matches!(expr, IrExpr::Array(_))
|| matches!(expr, IrExpr::TypeCast(c) if c.pg_type == "jsonb")
|| matches!(expr, IrExpr::NamedTuple { .. })
|| matches!(expr, IrExpr::Tuple(_))
|| matches!(expr, IrExpr::JsonbField { .. })
|| matches!(expr, IrExpr::JsonbIndex { .. })
}
fn emit_free_rows(sel: &IrSelect, rows: &[IrRowSource], ctes: &[IrCteDef]) -> SqlOutput {
use crate::query::ShapeNode;
let items: Vec<&IrFreeExpr> = rows
.iter()
.map(|r| match r {
IrRowSource::Free(item) => item,
IrRowSource::Bound { .. } => unreachable!("mixed Bound/Free rows rejected at compile time"),
})
.collect();
if items.is_empty() {
return SqlOutput {
sql: "SELECT NULL AS result WHERE FALSE".to_string(),
shape: ShapeDescriptor {
root: ShapeNode::Scalar {
name: String::new(),
position: 0,
},
},
inference_plan: None,
};
}
if items.len() == 1
&& let IrFreeExpr::AssertSet {
fn_name,
inner,
message,
} = items[0]
{
let array_sql = emit_array_source(inner) + &assert_message_arg(message);
let mut sql = format!(
"SELECT ROW(v) AS result FROM unnest(\"_pylon\".{}({})) AS _assert(v)",
fn_name, array_sql,
);
if sel.distinct {
sql = format!("SELECT DISTINCT * FROM ({}) AS \"_distinct\"", sql);
}
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
return SqlOutput {
sql,
shape: ShapeDescriptor {
root: ShapeNode::Scalar {
name: String::new(),
position: 0,
},
},
inference_plan: None,
};
}
let shape_root = free_item_shape(items.first().unwrap(), ctes);
let branches: Vec<String> = items
.iter()
.map(|item| match item {
IrFreeExpr::Scalar(expr) => {
if matches!(expr, IrExpr::ObjectSubquery(_)) {
return format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _obj", emit_expr(expr));
}
if is_raw_scalar(expr) {
format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _raw", emit_expr(expr))
} else {
let e = emit_expr(expr);
let row_value = if enum_type_of_expr(expr).is_some() {
"v::text"
} else {
"v"
};
let present = if items.len() > 1 { " WHERE v IS NOT NULL" } else { "" };
format!("SELECT ROW({row_value}) AS result, v FROM (SELECT {e} AS v) AS _scalar{present}")
}
}
IrFreeExpr::FreeObject(fields) => {
let inner_cols: Vec<String> = fields
.iter()
.enumerate()
.map(|(i, (_, e))| format!("{} AS \"_f{}\"", emit_expr(e), i))
.collect();
let row_items: Vec<String> = fields
.iter()
.enumerate()
.map(|(i, (_, e))| {
if enum_type_of_expr(e).is_some() {
format!("\"_f{}\"::text", i)
} else {
format!("\"_f{}\"", i)
}
})
.collect();
let named_cols: Vec<String> = fields
.iter()
.enumerate()
.map(|(i, (name, _))| format!("\"_f{}\" AS {}", i, qi(name)))
.collect();
format!(
"SELECT ROW({}) AS result, {} FROM (SELECT {}) AS _obj",
row_items.join(", "),
named_cols.join(", "),
inner_cols.join(", "),
)
}
IrFreeExpr::Tuple(exprs) => {
if exprs.len() == 1 {
format!("SELECT ROW({}) AS result", emit_free_field_expr(&exprs[0]))
} else {
let parts: Vec<String> = exprs.iter().map(emit_free_field_expr).collect();
format!("SELECT ({}) AS result", parts.join(", "))
}
}
IrFreeExpr::NamedTupleRow(fields) => {
let parts: Vec<String> = fields.iter().map(|(_, e)| emit_free_field_expr(e)).collect();
if parts.len() == 1 {
format!("SELECT ROW({}) AS result", parts[0])
} else {
format!("SELECT ({}) AS result", parts.join(", "))
}
}
IrFreeExpr::AssertSet { .. } => unreachable!("AssertSet is handled by early return above"),
IrFreeExpr::CtePassthrough(name) => format!("SELECT \"result\" FROM {}", qi(name)),
})
.collect();
let union_sql = branches.join("\nUNION ALL\n");
let mut sql = if sel.distinct {
format!("SELECT DISTINCT * FROM (\n{}\n) AS \"_distinct\"", union_sql)
} else {
union_sql
};
if let Some(filter) = &sel.filter {
sql = format!(
"SELECT * FROM (\n{}\n) AS \"_filtered\"\nWHERE {}",
sql,
emit_expr(filter)
);
}
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
SqlOutput {
sql,
shape: ShapeDescriptor { root: shape_root },
inference_plan: None,
}
}
fn enum_type_of_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
match expr {
IrExpr::ColumnRef { pg_type, .. } => QualifiedPgType::of(pg_type),
IrExpr::EnumLiteral { pg_type, .. } => Some(QualifiedPgType {
name: pg_quoted_to_pylon(pg_type),
is_array: false,
}),
_ => None,
}
}
fn enum_type_of_shape_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
if let Some(qualified) = enum_type_of_expr(expr) {
return Some(qualified);
}
let IrExpr::Subquery(select) = expr else {
return None;
};
let [IrRowSource::Bound { shape, .. }] = select.rows.as_slice() else {
return None;
};
let [IrShapePointer::Scalar(scalar)] = shape.as_slice() else {
return None;
};
QualifiedPgType::of(&scalar.pg_type)
}
fn emit_free_field_expr(expr: &IrExpr) -> String {
match enum_type_of_expr(expr) {
Some(qualified) => format!("{}{}", emit_expr(expr), qualified.text_cast()),
None => emit_expr(expr),
}
}
fn free_field_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
use crate::query::{Cardinality, ShapeNode};
if let IrExpr::ObjectSubquery(sel) = expr {
let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
unreachable!("an object subquery is always schema-bound")
};
let (_, nodes) = build_shape(shape, &source.alias);
return ShapeNode::Object {
name: name.to_string(),
type_name: Some(source.type_name.clone()),
position,
cardinality: Cardinality::Optional,
pointers: prepend_type(nodes),
has_implicit_id: shape_has_implicit_id(shape),
};
}
if matches!(expr, IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. }) {
return expr_shape_node(name, position, expr);
}
if let IrExpr::ArrayFromSelect(source) = expr
&& matches!(
source.as_ref(),
IrArraySource::ObjectSelect(_)
| IrArraySource::ObjectFunction(_)
| IrArraySource::PathSelect(_)
| IrArraySource::Group(_)
)
{
return expr_shape_node(name, position, expr);
}
match enum_type_of_shape_expr(expr) {
Some(qualified) => qualified.shape_node(name.to_string(), position),
None => ShapeNode::Scalar {
name: name.to_string(),
position,
},
}
}
fn expr_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
use crate::query::{Cardinality, ShapeNode};
match expr {
IrExpr::ObjectPathUnion { branches, multi, .. } => {
let first = branches.first().expect("a union has at least one branch");
let IrPathResult::Object {
alias,
type_name,
shape,
} = &first.result
else {
unreachable!("an object path union's branches always land on objects")
};
let (_, pointer_nodes) = build_shape(shape, alias);
let object = ShapeNode::Object {
name: if *multi { String::new() } else { name.to_string() },
type_name: Some(type_name.clone()),
position: if *multi { 0 } else { position },
cardinality: if *multi {
Cardinality::Many
} else {
Cardinality::Optional
},
pointers: prepend_type(pointer_nodes),
has_implicit_id: shape_has_implicit_id(shape),
};
if *multi {
ShapeNode::Array {
name: name.to_string(),
position,
element: Box::new(object),
}
} else {
object
}
}
IrExpr::ObjectPathSubquery(ps) => {
let IrPathResult::Object {
alias,
type_name,
shape,
} = &ps.result
else {
unreachable!("an object path subquery always lands on an object")
};
let (_, pointer_nodes) = build_shape(shape, alias);
ShapeNode::Object {
name: name.to_string(),
type_name: Some(type_name.clone()),
position,
cardinality: Cardinality::Optional,
pointers: prepend_type(pointer_nodes),
has_implicit_id: shape_has_implicit_id(shape),
}
}
IrExpr::TypeCast(c) if c.tuple_shape.is_some() => {
let shape = c.tuple_shape.as_ref().unwrap();
ShapeNode::NamedTuple {
name: name.to_string(),
position,
type_name: shape.type_name.clone(),
members: Some(shape.members.clone()),
is_free_object: false,
}
}
IrExpr::TypeCast(c) if c.pg_type == "jsonb" => ShapeNode::Scalar {
name: name.to_string(),
position,
},
IrExpr::NamedTuple { is_free_object, .. } => ShapeNode::NamedTuple {
name: name.to_string(),
position,
type_name: None,
members: None,
is_free_object: *is_free_object,
},
IrExpr::ArrayFromSelect(source) => match source.as_ref() {
IrArraySource::ObjectFunction(fs) => {
let (_, nodes) = build_shape(&fs.shape, &fs.alias);
ShapeNode::Array {
name: name.to_string(),
position,
element: Box::new(ShapeNode::Object {
name: String::new(),
type_name: Some(fs.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(nodes),
has_implicit_id: shape_has_implicit_id(&fs.shape),
}),
}
}
IrArraySource::Group(grp) => ShapeNode::Array {
name: name.to_string(),
position,
element: Box::new(emit_group(grp).shape.root),
},
IrArraySource::ObjectSelect(sel) => {
let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
unreachable!("IrArraySource::ObjectSelect is always schema-bound")
};
let (_, nodes) = build_shape(shape, &source.alias);
ShapeNode::Array {
name: name.to_string(),
position,
element: Box::new(ShapeNode::Object {
name: String::new(),
type_name: Some(source.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(nodes),
has_implicit_id: shape_has_implicit_id(shape),
}),
}
}
IrArraySource::PathSelect(ps) => match &ps.result {
IrPathResult::Object {
alias,
type_name,
shape,
} => {
let (_, pointer_nodes) = build_shape(shape, alias);
ShapeNode::Array {
name: name.to_string(),
position,
element: Box::new(ShapeNode::Object {
name: String::new(),
type_name: Some(type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(pointer_nodes),
has_implicit_id: shape_has_implicit_id(shape),
}),
}
}
IrPathResult::Scalar(..) => ShapeNode::Scalar {
name: name.to_string(),
position,
},
},
_ => ShapeNode::Scalar {
name: name.to_string(),
position,
},
},
e if is_raw_scalar(e) => ShapeNode::RawScalar,
e => free_field_shape_node(name, position, e),
}
}
fn free_item_shape(item: &IrFreeExpr, ctes: &[IrCteDef]) -> crate::query::ShapeNode {
use crate::query::{Cardinality, ShapeNode};
match item {
IrFreeExpr::Scalar(e) => match e {
IrExpr::TypeCast(c) if c.pg_type == "jsonb" && c.tuple_shape.is_none() => ShapeNode::JsonScalar,
_ => expr_shape_node("", 0, e),
},
IrFreeExpr::FreeObject(fields) => ShapeNode::Object {
name: String::new(),
type_name: None,
position: 0,
cardinality: Cardinality::Many,
pointers: fields
.iter()
.enumerate()
.map(|(i, (name, e))| free_field_shape_node(name, i, e))
.collect(),
has_implicit_id: false,
},
IrFreeExpr::Tuple(exprs) => ShapeNode::Tuple {
position: 0,
elements: exprs
.iter()
.enumerate()
.map(|(i, e)| free_field_shape_node("", i, e))
.collect(),
names: None,
},
IrFreeExpr::NamedTupleRow(fields) => ShapeNode::Tuple {
position: 0,
elements: fields
.iter()
.enumerate()
.map(|(i, (name, e))| free_field_shape_node(name, i, e))
.collect(),
names: Some(fields.iter().map(|(name, _)| name.clone()).collect()),
},
IrFreeExpr::AssertSet { .. } => ShapeNode::Scalar {
name: String::new(),
position: 0,
},
IrFreeExpr::CtePassthrough(name) => ctes
.iter()
.find(|c| &c.name == name)
.and_then(|c| match &c.stmt {
IrStmt::Select(sel) => match sel.rows.first() {
Some(IrRowSource::Free(inner)) => Some(free_item_shape(inner, ctes)),
_ => None,
},
_ => None,
})
.unwrap_or(ShapeNode::Scalar {
name: String::new(),
position: 0,
}),
}
}
fn emit_path_joins(root: &IrSource, joins: &[IrPathJoin]) -> String {
let mut parts = vec![format!("{} AS {}", source_ref(root), qi(&root.alias))];
for join in joins {
match join {
IrPathJoin::Single {
source_alias,
fk_col,
target,
} => {
parts.push(format!(
"JOIN {} AS {} ON {}.{} = {}.\"id\"",
source_ref(target),
qi(&target.alias),
qi(source_alias),
qi(fk_col),
qi(&target.alias),
));
}
IrPathJoin::Multi {
source_alias,
junction_alias,
join,
target,
} => {
match join {
IrMultiLinkJoin::Standard { junction_table, module } => {
parts.push(format!(
"JOIN {} AS {} ON {}.\"source\" = {}.\"id\"",
junction_ref(module, junction_table),
qi(junction_alias),
qi(junction_alias),
qi(source_alias),
));
parts.push(format!(
"JOIN {} AS {} ON {}.\"id\" = {}.\"target\"",
source_ref(target),
qi(&target.alias),
qi(&target.alias),
qi(junction_alias),
));
}
IrMultiLinkJoin::Through {
junction_table,
module,
source_col,
target_col,
} => {
parts.push(format!(
"JOIN {} AS {} ON {}.{} = {}.\"id\"",
junction_ref(module, junction_table),
qi(junction_alias),
qi(junction_alias),
qi(source_col),
qi(source_alias),
));
parts.push(format!(
"JOIN {} AS {} ON {}.\"id\" = {}.{}",
source_ref(target),
qi(&target.alias),
qi(&target.alias),
qi(junction_alias),
qi(target_col),
));
}
IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
unreachable!("a forward multi-link path step never uses a backlink join variant")
}
}
}
IrPathJoin::BacklinkSingle {
source_alias,
fk_col,
target,
} => {
parts.push(format!(
"JOIN {} AS {} ON {}.{} = {}.\"id\"",
source_ref(target),
qi(&target.alias),
qi(&target.alias),
qi(fk_col),
qi(source_alias),
));
}
IrPathJoin::Function {
fn_module,
fn_name,
args,
target,
} => {
let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
parts.push(format!(
"CROSS JOIN LATERAL {}.{}({}) AS {}",
pg_schema(fn_module),
qi(fn_name),
args_sql,
qi(&target.alias),
));
}
IrPathJoin::Lateral { inner, target } => {
let projected = match &inner.result {
IrPathResult::Object { alias, .. } => alias.clone(),
IrPathResult::Scalar(..) => {
unreachable!("a lateral step is only built for an object-valued computed")
}
};
let mut body = format!(
"SELECT {}.* FROM {}",
qi(&projected),
emit_path_joins(&inner.root, &inner.joins)
);
append_filter(&mut body, &inner.filter);
append_order_by(&mut body, &inner.order_by);
append_offset_limit(&mut body, &inner.offset, &inner.limit);
parts.push(format!("JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias)));
}
IrPathJoin::BacklinkMulti {
source_alias,
junction_alias,
junction_table,
module,
owner_col,
current_col,
target,
} => {
parts.push(format!(
"JOIN {} AS {} ON {}.{} = {}.\"id\"",
junction_ref(module, junction_table),
qi(junction_alias),
qi(junction_alias),
qi(current_col),
qi(source_alias),
));
parts.push(format!(
"JOIN {} AS {} ON {}.\"id\" = {}.{}",
source_ref(target),
qi(&target.alias),
qi(&target.alias),
qi(junction_alias),
qi(owner_col),
));
}
}
}
parts.join("\n")
}
fn emit_array_source(src: &IrArraySource) -> String {
match src {
IrArraySource::Select(s) => {
let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
unreachable!("IrArraySource::Select is always schema-bound")
};
let scalar = match shape.first() {
Some(IrShapePointer::Scalar(sf)) => format!("{}.{}", qi(&source.alias), qi(&sf.column)),
_ => format!("{}.\"id\"", qi(&source.alias)),
};
let mut sql = format!("SELECT {} FROM {} AS {}", scalar, source_ref(source), qi(&source.alias));
append_filter(&mut sql, &s.filter);
format!("ARRAY({})", sql)
}
IrArraySource::ObjectFunction(fs) => {
let (exprs, _) = build_shape(&fs.shape, &fs.alias);
let mut parts = vec![sql_str(&fs.type_name) + "::text"];
parts.extend(exprs);
let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
let mut sql = format!(
"SELECT (\n {}\n) FROM {}.{}({}) AS {}",
parts.join(",\n "),
pg_schema(&fs.fn_module),
qi(&fs.fn_name),
args_sql,
qi(&fs.alias),
);
append_filter(&mut sql, &fs.filter);
append_order_by(&mut sql, &fs.order_by);
append_offset_limit(&mut sql, &fs.offset, &fs.limit);
format!("ARRAY({})", sql)
}
IrArraySource::ObjectSelect(s) => {
let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
unreachable!("IrArraySource::ObjectSelect is always schema-bound")
};
let (exprs, _) = build_shape(shape, &source.alias);
let mut parts = vec![source_type_disc(source)];
parts.extend(exprs);
let mut sql = format!(
"SELECT (\n {}\n) FROM {} AS {}",
parts.join(",\n "),
source_ref(source),
qi(&source.alias)
);
append_filter(&mut sql, &s.filter);
append_order_by(&mut sql, &s.order_by);
append_offset_limit(&mut sql, &s.offset, &s.limit);
format!("ARRAY({})", sql)
}
IrArraySource::Group(grp) => format!("ARRAY({})", emit_group(grp).sql),
IrArraySource::StmtColumn { stmt, column } => format!(
"ARRAY(SELECT {} FROM (\n{}\n) AS \"_rows\")",
qi(column),
emit_dml_as_cte_source(stmt),
),
IrArraySource::PathSelect(ps) => {
let scalar = match &ps.result {
IrPathResult::Scalar(e, _) => emit_expr(e),
IrPathResult::Object {
alias,
type_name,
shape,
} => {
let (pointer_exprs, _) = build_shape(shape, alias);
let mut parts = vec![result_type_disc(ps, alias, type_name)];
parts.extend(pointer_exprs);
format!("(\n {}\n)", parts.join(",\n "))
}
};
let from_sql = emit_path_joins(&ps.root, &ps.joins);
let mut sql = format!(
"SELECT {}{} FROM {}",
if ps.distinct { "DISTINCT " } else { "" },
scalar,
from_sql
);
append_filter(&mut sql, &ps.filter);
append_order_by(&mut sql, &ps.order_by);
append_offset_limit(&mut sql, &ps.offset, &ps.limit);
format!("ARRAY({})", sql)
}
IrArraySource::RawExpr {
source,
poly_implementors,
poly_columns,
expr,
} => {
let from_sql = if !poly_implementors.is_empty() {
format!(
"(\n{}\n) AS {}",
emit_poly_union(poly_implementors, poly_columns),
qi(&source.alias)
)
} else {
format!("{} AS {}", source_ref(source), qi(&source.alias))
};
format!("ARRAY(SELECT {} FROM {})", emit_expr(expr), from_sql)
}
}
}
fn emit_key_expr(expr: &IrExpr) -> String {
if let IrExpr::ColumnRef { alias, column, pg_type } = expr
&& let Some(qualified) = QualifiedPgType::of(pg_type)
{
let col_ref = if alias.is_empty() {
qi(column)
} else {
format!("{}.{}", qi(alias), qi(column))
};
return format!("{}{}", col_ref, qualified.text_cast());
}
emit_expr(expr)
}
fn group_rows(grp: &IrGroup) -> (String, Option<String>) {
let alias = &grp.source.alias;
if grp.limit.is_none() && grp.offset.is_none() {
return (
format!("{} AS {}", source_ref(&grp.source), qi(alias)),
grp.filter.as_ref().map(emit_expr),
);
}
let ranked = format!(
"(SELECT {}.*, row_number() OVER (PARTITION BY {}{}) AS \"__rk\"\n FROM {} AS {}{}) AS {}",
qi(alias),
group_by_sql(grp),
group_element_order(grp),
source_ref(&grp.source),
qi(alias),
grp.filter
.as_ref()
.map(|f| format!("\n WHERE {}", emit_expr(f)))
.unwrap_or_default(),
qi(alias),
);
let lower = grp.offset.as_ref().map(emit_expr).unwrap_or_else(|| "0".to_string());
let mut conds = vec![format!("\"__rk\" > {}", lower)];
if let Some(l) = &grp.limit {
conds.push(format!("\"__rk\" <= {} + {}", lower, emit_expr(l)));
}
(ranked, Some(conds.join(" AND ")))
}
fn group_by_sql(grp: &IrGroup) -> String {
grp.keys
.iter()
.map(|(_, key_expr)| emit_expr(key_expr))
.collect::<Vec<_>>()
.join(", ")
}
fn group_element_order(grp: &IrGroup) -> String {
if grp.order_by.is_empty() {
return String::new();
}
let s: Vec<_> = grp.order_by.iter().map(emit_sort_clause).collect();
format!(" ORDER BY {}", s.join(", "))
}
fn emit_group_elements_source(grp: &IrGroup) -> String {
let (from_sql, cond) = group_rows(grp);
let mut sql = format!("SELECT {}.*\n FROM {}", qi(&grp.source.alias), from_sql);
if let Some(cond) = cond {
sql.push_str(&format!("\n WHERE {}", cond));
}
sql
}
fn emit_group(grp: &IrGroup) -> SqlOutput {
match &grp.output {
IrGroupOutput::Groups => emit_group_rows(grp),
IrGroupOutput::Projection(projection) => emit_group_projection(
grp,
&projection.pointers,
&projection.order_by,
&projection.offset,
&projection.limit,
),
IrGroupOutput::Elements => emit_group_elements(grp),
}
}
fn emit_group_projection(
grp: &IrGroup,
pointers: &[IrShapePointer],
order_by: &[IrSort],
offset: &Option<IrExpr>,
limit: &Option<IrExpr>,
) -> SqlOutput {
let (exprs, nodes) = build_shape(pointers, &grp.source.alias);
let mut parts = vec!["NULL::text".to_string()];
parts.extend(exprs);
let (from_sql, cond) = group_rows(grp);
let mut sql = format!(
"SELECT (\n {}\n) AS \"result\"\nFROM {}",
parts.join(",\n "),
from_sql
);
if let Some(cond) = cond {
sql.push_str(&format!("\nWHERE {}", cond));
}
sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
if !order_by.is_empty() {
let sorts: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
sql.push_str(&format!("\nORDER BY {}", sorts.join(", ")));
}
append_offset_limit(&mut sql, offset, limit);
SqlOutput {
sql,
shape: ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: None,
position: 0,
cardinality: Cardinality::Many,
pointers: nodes,
has_implicit_id: false,
},
},
inference_plan: None,
}
}
fn emit_group_elements(grp: &IrGroup) -> SqlOutput {
let (exprs, nodes) = build_shape(&grp.shape, &grp.source.alias);
let mut parts = vec![source_type_disc(&grp.source)];
parts.extend(exprs);
let (from_sql, cond) = group_rows(grp);
let mut sql = format!(
"SELECT (\n {}\n) AS \"result\"\nFROM {}",
parts.join(",\n "),
from_sql
);
if let Some(cond) = cond {
sql.push_str(&format!("\nWHERE {}", cond));
}
SqlOutput {
sql,
shape: ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: Some(grp.source.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(nodes),
has_implicit_id: shape_has_implicit_id(&grp.shape),
},
},
inference_plan: None,
}
}
fn emit_group_rows(grp: &IrGroup) -> SqlOutput {
let alias = &grp.source.alias;
let (shape_exprs, shape_nodes) = build_shape(&grp.shape, alias);
let mut elem_row_parts = vec![source_type_disc(&grp.source)];
elem_row_parts.extend(shape_exprs);
let elem_row = elem_row_parts.join(",\n ");
let n_keys = grp.keys.len();
let grouping_pos = n_keys + 1;
let elements_pos = n_keys + 2;
let mut key_exprs_sql: Vec<String> = vec![];
let mut key_nodes: Vec<ShapeNode> = vec![];
for (i, (key_name, key_expr)) in grp.keys.iter().enumerate() {
let pos = i + 1;
if let IrExpr::ColumnRef { pg_type, .. } = key_expr
&& let Some(qualified) = QualifiedPgType::of(pg_type)
{
key_exprs_sql.push(emit_key_expr(key_expr));
key_nodes.push(qualified.shape_node(key_name.clone(), pos));
continue;
}
key_exprs_sql.push(emit_expr(key_expr));
key_nodes.push(ShapeNode::Scalar {
name: key_name.clone(),
position: pos,
});
}
let mut outer_parts = vec!["NULL::text".to_string()];
outer_parts.extend(key_exprs_sql.clone());
let key_names_sql = grp
.keys
.iter()
.map(|(name, _)| format!("'{}'", name))
.collect::<Vec<_>>()
.join(", ");
outer_parts.push(format!("ARRAY[{}]::text[]", key_names_sql));
outer_parts.push(format!(
"array_agg(ROW(\n {}\n )::record{})",
elem_row,
group_element_order(grp)
));
let outer_tuple = outer_parts.join(",\n ");
let (from_sql, cond) = group_rows(grp);
let mut sql = format!("SELECT (\n {}\n) AS \"result\"\nFROM {}", outer_tuple, from_sql,);
if let Some(cond) = cond {
sql.push_str(&format!("\nWHERE {}", cond));
}
sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
let element_node = ShapeNode::Object {
name: String::new(),
type_name: Some(grp.source.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(shape_nodes),
has_implicit_id: shape_has_implicit_id(&grp.shape),
};
let root = ShapeNode::Group {
key_nodes,
grouping_position: grouping_pos,
elements_position: elements_pos,
element: Box::new(element_node),
};
SqlOutput {
sql,
shape: ShapeDescriptor { root },
inference_plan: None,
}
}
fn emit_poly_union_type_only(implementors: &[IrPolyImplementor]) -> String {
implementors
.iter()
.map(|imp| {
format!(
" SELECT {}::text AS \"__type__\" FROM {}",
sql_str(&imp.type_name),
qn(&imp.module, &imp.table),
)
})
.collect::<Vec<_>>()
.join("\n UNION ALL\n")
}
fn emit_path_select(sel: &IrPathSelect) -> SqlOutput {
let distinct = if sel.distinct { "DISTINCT " } else { "" };
let from_sql = if !sel.poly_implementors.is_empty() {
format!(
"(\n{}\n) AS {}",
emit_poly_union_type_only(&sel.poly_implementors),
qi(&sel.root.alias),
)
} else {
emit_path_joins(&sel.root, &sel.joins)
};
let (result_expr, shape_root) = match &sel.result {
IrPathResult::Scalar(ir_expr, tuple_shape) => {
let is_nt = matches!(ir_expr, IrExpr::NamedTuple { .. })
|| matches!(ir_expr, IrExpr::Tuple(_))
|| matches!(ir_expr, IrExpr::JsonbField { .. })
|| matches!(ir_expr, IrExpr::JsonbIndex { .. })
|| matches!(ir_expr, IrExpr::ColumnRef { pg_type, .. } if pg_type.starts_with("__nt__:"))
|| tuple_shape.is_some();
if is_nt {
let expr_sql = format!("{} AS result", emit_expr(ir_expr));
let shape = if matches!(
ir_expr,
IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::Tuple(_)
) {
ShapeNode::RawScalar
} else if let Some(shape) = tuple_shape {
ShapeNode::NamedTuple {
name: String::new(),
position: 0,
type_name: shape.type_name.clone(),
members: Some(shape.members.clone()),
is_free_object: false,
}
} else {
let type_name = match ir_expr {
IrExpr::ColumnRef { pg_type, .. } => pg_type.strip_prefix("__nt__:").map(|s| s.to_string()),
_ => None,
};
ShapeNode::NamedTuple {
name: String::new(),
position: 0,
type_name,
members: None,
is_free_object: false,
}
};
(expr_sql, shape)
} else {
if let IrExpr::ColumnRef { pg_type, .. } = ir_expr {
if let Some(qualified) = QualifiedPgType::of(pg_type) {
let expr = format!("ROW({}{}) AS result", emit_expr(ir_expr), qualified.text_cast());
let shape = qualified.shape_node(String::new(), 0);
(expr, shape)
} else {
let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
(
expr,
ShapeNode::Scalar {
name: String::new(),
position: 0,
},
)
}
} else {
let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
(
expr,
ShapeNode::Scalar {
name: String::new(),
position: 0,
},
)
}
}
}
IrPathResult::Object {
alias,
type_name,
shape,
} => {
let (pointer_exprs, pointer_nodes) = build_shape(shape, alias);
let mut parts = vec![result_type_disc(sel, alias, type_name)];
parts.extend(pointer_exprs);
let expr = format!("(\n {}\n) AS result", parts.join(",\n "));
let shape_root = ShapeNode::Object {
name: String::new(),
type_name: Some(type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(pointer_nodes),
has_implicit_id: shape_has_implicit_id(shape),
};
(expr, shape_root)
}
};
let mut sql = format!("SELECT {}{}\nFROM {}", distinct, result_expr, from_sql);
append_filter(&mut sql, &sel.filter);
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
SqlOutput {
sql,
shape: ShapeDescriptor { root: shape_root },
inference_plan: None,
}
}
fn yields_jsonb(expr: &IrExpr) -> bool {
match expr {
IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::NamedTuple { .. } => true,
IrExpr::TypeCast(c) => c.pg_type == "jsonb",
IrExpr::ColumnRef { pg_type, .. } | IrExpr::FnParam { pg_type, .. } => pg_type == "jsonb",
IrExpr::CteRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => pg_type.as_deref() == Some("jsonb"),
IrExpr::FunctionCall(f) if f.return_pg_type.is_some() => f.return_pg_type.as_deref() == Some("jsonb"),
IrExpr::FunctionCall(f) if f.schema.is_none() => {
let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Json))
}
_ => false,
}
}
fn emit_for_iterator(it: &IrForIterator, iter_alias: &str) -> (String, String) {
match it {
IrForIterator::Values { exprs, pg_type } => {
let rows: Vec<String> = exprs
.iter()
.map(|e| format!("({}::{})", emit_expr(e), pg_type))
.collect();
(
format!("(VALUES {}) AS {}(\"v\")", rows.join(", "), qi(iter_alias)),
format!("{}(\"v\") AS (VALUES {})", qi(iter_alias), rows.join(", ")),
)
}
IrForIterator::Query { stmt, scalar } => {
let inner = emit_dml_as_cte_source(stmt);
let body = if *scalar {
inner
} else {
format!(" SELECT \"id\" AS v FROM (\n{}\n ) AS _src", inner)
};
(
format!("(\n{}\n) AS {}", body, qi(iter_alias)),
format!("{} AS (\n{}\n)", qi(iter_alias), body),
)
}
IrForIterator::SetReturning { expr, pg_type } => {
let body = format!(" SELECT {}::{} AS v", emit_expr(expr), pg_type);
(
format!("(\n{}\n) AS {}", body, qi(iter_alias)),
format!("{} AS (\n{}\n)", qi(iter_alias), body),
)
}
}
}
fn emit_nested_for_iterator(it: &IrForIterator, iter_alias: &str, outer_alias: &str) -> String {
const OUTER: &str = "_outer";
match it {
IrForIterator::Values { exprs, pg_type } => {
let rows: Vec<String> = exprs
.iter()
.map(|e| format!("({}::{})", emit_expr(e), pg_type))
.collect();
format!(
"{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, (VALUES {}) AS \"_vals\"(\"v\")\n)",
qi(iter_alias),
qi(outer_alias),
qi(OUTER),
qi(outer_alias),
rows.join(", "),
)
}
IrForIterator::Query { stmt, scalar } => {
let inner = emit_dml_as_cte_source(stmt);
let projected = if *scalar { "\"_src\".\"v\"" } else { "\"_src\".\"id\"" };
format!(
"{} AS (\nSELECT {}.\"v\" AS {}, {} AS v\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_src\"\n)",
qi(iter_alias),
qi(outer_alias),
qi(OUTER),
projected,
qi(outer_alias),
inner,
)
}
IrForIterator::SetReturning { expr, pg_type } => format!(
"{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, LATERAL (SELECT {}::{} AS v) AS \"_vals\"\n)",
qi(iter_alias),
qi(outer_alias),
qi(OUTER),
qi(outer_alias),
emit_expr(expr),
pg_type,
),
}
}
fn nested_for_from(inner_alias: &str, outer_alias: &str) -> String {
format!(
"{} JOIN {} ON {}.\"v\" = {}.\"_outer\"",
qi(inner_alias),
qi(outer_alias),
qi(outer_alias),
qi(inner_alias),
)
}
fn uncorrelated_body_ctes(f: &IrFor) -> Vec<IrCteDef> {
f.body_ctes
.iter()
.map(|cte| IrCteDef {
correlated_to: None,
..cte.clone()
})
.collect()
}
fn prefix_body_ctes(sql: String, body_ctes: &[IrCteDef]) -> String {
if body_ctes.is_empty() {
return sql;
}
merge_into_existing_with(&sql, &emit_user_cte_parts(body_ctes))
.unwrap_or_else(|| format!("{}{}", emit_cte_prefix(body_ctes), sql))
}
fn emit_for_stmt(f: &IrFor, user_ctes: &[IrCteDef]) -> SqlOutput {
let iter_alias = format!("_for_{}", f.var_name);
if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
let empty = SqlOutput {
sql: "SELECT NULL AS result WHERE FALSE".to_string(),
shape: ShapeDescriptor {
root: ShapeNode::Scalar {
name: String::new(),
position: 0,
},
},
inference_plan: None,
};
return empty;
}
let (values_from, iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
match f.body.as_ref() {
IrStmt::Insert(ins) => emit_for_insert(ins, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
IrStmt::Update(upd) => emit_for_update(upd, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
body => {
let body_out = match body {
IrStmt::Select(sel) => emit_select_stmt(sel, user_ctes),
IrStmt::PathSelect(sel) => emit_path_select(sel),
other => unreachable!("for-loop body should have been rejected at compile time: {other:?}"),
};
let indent_body = prefix_body_ctes(body_out.sql, &uncorrelated_body_ctes(f)).replace('\n', "\n ");
let cte_prefix = if !user_ctes.is_empty() {
emit_cte_prefix(user_ctes)
} else {
String::new()
};
let sql = format!(
"{}SELECT \"_body\".result\nFROM {}\nCROSS JOIN LATERAL (\n {}\n) AS \"_body\"",
cte_prefix, values_from, indent_body,
);
SqlOutput {
sql,
shape: body_out.shape,
inference_plan: None,
}
}
}
}
fn emit_for_update(
upd: &IrUpdate,
iter_alias: &str,
iter_cte: &str,
user_ctes: &[IrCteDef],
body_ctes: &[IrCteDef],
) -> SqlOutput {
let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
cte_parts.extend(emit_user_cte_parts(body_ctes));
cte_parts.push(iter_cte.to_string());
let alias = &upd.target.alias;
if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
const ITER_COL: &str = "_iter";
let ids_name = "_ids";
let generated: Vec<Option<&IrInsert>> = upd
.multi_link_appends
.iter()
.map(|a| per_iteration_insert(a, &upd.nested_ctes))
.collect();
let new_cols: String = generated
.iter()
.enumerate()
.filter_map(|(i, ins)| {
ins.map(|ins| {
format!(
", {} AS {}",
ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
qi(&format!("_new_{i}"))
)
})
})
.collect();
let mut ids_sql = format!(
"\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
ids_name,
qi(alias),
qi(iter_alias),
qi(ITER_COL),
new_cols,
target_ref(&upd.target),
qi(alias),
qi(iter_alias),
);
append_filter(&mut ids_sql, &upd.filter);
ids_sql.push_str("\n)");
cte_parts.push(ids_sql);
for (i, append) in upd.multi_link_appends.iter().enumerate() {
match generated[i] {
Some(ins) => {
cte_parts.push(emit_for_nested_insert_cte(
ins,
ids_name,
iter_alias,
&format!("_ml_ins_{i}"),
&format!("_new_{i}"),
));
cte_parts.push(emit_for_ml_append_cte(
append,
ids_name,
&format!("_ml_add_{i}"),
&format!("_new_{i}"),
));
}
None => cte_parts.push(emit_for_ml_append_cte(
append,
ids_name,
&format!("_ml_add_{i}"),
ITER_COL,
)),
}
}
let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, ids_name);
let mut parts = vec![type_disc(&upd.target.type_name)];
parts.extend(pointer_exprs);
let sql = format!(
"WITH {}\nSELECT (\n {}\n) AS result\nFROM \"{}\"",
cte_parts.join(",\n"),
parts.join(",\n "),
ids_name,
);
return SqlOutput {
sql,
shape: crate::query::ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: Some(upd.target.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(shape_nodes),
has_implicit_id: shape_has_implicit_id(&upd.returning),
},
},
inference_plan: None,
};
}
let sets: Vec<String> = upd
.assignments
.iter()
.map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
.chain(
upd.rewrites
.iter()
.map(|rw| format!("{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
)
.collect();
let mut sql = format!(
"WITH {}\nUPDATE {} AS {}\nSET {}\nFROM {}",
cte_parts.join(",\n"),
target_ref(&upd.target),
qi(alias),
sets.join(", "),
qi(iter_alias),
);
append_filter(&mut sql, &upd.filter);
let mut parts = vec![type_disc(&upd.target.type_name)];
let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, alias);
parts.extend(pointer_exprs);
sql.push_str(&format!("\nRETURNING (\n {}\n) AS result", parts.join(",\n ")));
SqlOutput {
sql,
shape: crate::query::ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: Some(upd.target.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: prepend_type(shape_nodes),
has_implicit_id: shape_has_implicit_id(&upd.returning),
},
},
inference_plan: None,
}
}
const OUTER_KEY: &str = "_outer";
fn emit_iteration_rows_cte(ins: &IrInsert, cte_name: &str, iter_alias: &str, outer_source: &str) -> String {
let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
let mut selected = vec![
format!(
"{} AS {}",
ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
qi("id")
),
format!("{} AS {}", outer_source, qi(OUTER_KEY)),
];
selected.extend(
ins.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(c, e)| format!("{} AS {}", emit_expr(e), qi(c)))
.chain(
ins.rewrites
.iter()
.map(|r| format!("{} AS {}", emit_expr(&r.expr), qi(&r.column))),
),
);
format!(
"\"{}\" AS (\nSELECT {}\nFROM {}\n)",
cte_name,
selected.join(", "),
qi(iter_alias),
)
}
fn emit_insert_from_rows_cte(ins: &IrInsert, cte_name: &str, rows_name: &str) -> String {
let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
let cols: Vec<String> = std::iter::once(qi("id"))
.chain(
ins.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(c, _)| qi(c)),
)
.chain(ins.rewrites.iter().map(|r| qi(&r.column)))
.collect();
let mut sql = format!(
"INSERT INTO {} ({})\nSELECT {} FROM \"{}\"",
target_ref(&ins.target),
cols.join(", "),
cols.join(", "),
rows_name,
);
if let Some(conflict) = &ins.unless_conflict {
emit_conflict(&mut sql, conflict);
}
sql.push_str("\nRETURNING *");
format!("\"{}\" AS (\n{}\n)", cte_name, sql)
}
fn emit_correlated_ml_append_cte(
mutation: &IrMultiLinkMutation,
rows_name: &str,
targets_name: &str,
cte_name: &str,
) -> String {
format!(
"\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"_src\".{}, \"_tgt\".{} FROM \"{}\" AS \"_src\" JOIN \"{}\" AS \"_tgt\" ON \"_tgt\".{} = \"_src\".{}\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
cte_name,
qn(&mutation.module, &mutation.junction_table),
qi(&mutation.source_col),
qi(&mutation.target_col),
qi("id"),
qi("id"),
rows_name,
targets_name,
qi(OUTER_KEY),
qi(OUTER_KEY),
qi(&mutation.source_col),
qi(&mutation.target_col),
)
}
fn correlated_append_indices(appends: &[IrMultiLinkMutation], defs: &[&[IrCteDef]]) -> Vec<usize> {
appends
.iter()
.enumerate()
.filter(|(_, append)| match &append.values.source {
IrMultiLinkValueSource::CteRef(target) => defs
.iter()
.flat_map(|group| group.iter())
.any(|c| &c.name == target && c.correlated_to.is_some()),
_ => false,
})
.map(|(i, _)| i)
.collect()
}
fn emit_for_insert(
ins: &IrInsert,
iter_alias: &str,
iter_cte: &str,
user_ctes: &[IrCteDef],
body_ctes: &[IrCteDef],
) -> SqlOutput {
let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
let cols: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(c, _)| qi(c))
.chain(ins.rewrites.iter().map(|r| qi(&r.column)))
.collect();
let sel_exprs: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(_, e)| emit_expr(e))
.chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
.collect();
let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
cte_parts.push(iter_cte.to_string());
cte_parts.extend(emit_user_cte_parts(body_ctes));
cte_parts.extend(emit_user_cte_parts(&ins.nested_ctes));
let correlated_targets = correlated_append_indices(&ins.multi_link_appends, &[body_ctes, &ins.nested_ctes]);
let mut insert_sql = format!(
"INSERT INTO {} ({})\nSELECT {} FROM {}",
target_ref(&ins.target),
cols.join(", "),
sel_exprs.join(", "),
qi(iter_alias),
);
if let Some(conflict) = &ins.unless_conflict {
emit_conflict(&mut insert_sql, conflict);
}
let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
if ins.multi_link_appends.is_empty() {
let mut sql = format!("WITH {}\n{}", cte_parts.join(",\n"), insert_sql);
if let Some(r) = returning_sql {
sql.push_str(&r);
}
return SqlOutput {
sql,
shape,
inference_plan: None,
};
}
const IDS: &str = "_for_dml__ids";
const ROWS: &str = "_for_dml__rows";
if correlated_targets.is_empty() {
insert_sql.push_str("\nRETURNING *");
cte_parts.push(format!("\"{}\" AS (\n{}\n)", IDS, insert_sql));
} else {
let outer_source = format!("{}.{}", qi(iter_alias), qi("v"));
cte_parts.push(emit_iteration_rows_cte(ins, ROWS, iter_alias, &outer_source));
cte_parts.push(emit_insert_from_rows_cte(ins, IDS, ROWS));
}
for (i, append) in ins.multi_link_appends.iter().enumerate() {
let cte_name = format!("_for_dml__ml_add_{}", i);
if correlated_targets.contains(&i) {
let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
unreachable!("only a CTE-backed value can carry an iteration key")
};
cte_parts.push(emit_correlated_ml_append_cte(append, ROWS, target, &cte_name));
} else {
cte_parts.push(emit_ml_append_cte(append, IDS, &ins.target.alias, &cte_name));
}
}
let projection = match &returning_sql {
Some(r) => r.trim_start_matches('\n').replacen("RETURNING ", "SELECT ", 1),
None => "SELECT 1".to_string(),
};
SqlOutput {
sql: format!("WITH {}\n{}\nFROM \"{}\"", cte_parts.join(",\n"), projection, IDS),
shape,
inference_plan: None,
}
}
fn enqueue_cte_sql(eq: &VectorEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
let index_name_sql = match &eq.index_name {
None => "NULL".to_string(),
Some(name) => sql_str(name),
};
format!(
concat!(
"\"{}\" AS (\n",
" INSERT INTO _pylon.\"IndexOutbox\"\n",
" (object_id, type_name, index_kind, index_name)\n",
" SELECT \"id\", {}, 'Vector'::_pylon.\"IndexKind\", {}\n",
" FROM \"{}\"\n",
" ON CONFLICT (object_id, index_kind, index_name)\n",
" DO UPDATE SET status = 'Pending', enqueued_at = now()\n",
")",
),
cte_name,
sql_str(&eq.type_name),
index_name_sql,
source_cte,
)
}
fn enqueue_ctes(enqueue: &[VectorEnqueueInfo], source_cte: &str) -> Vec<String> {
enqueue
.iter()
.enumerate()
.map(|(i, eq)| enqueue_cte_sql(eq, source_cte, &format!("_eq{}", i)))
.collect()
}
fn search_backend_index_kind(backend: &crate::schema::SearchBackend) -> &'static str {
match backend {
crate::schema::SearchBackend::OpenSearch => "OpenSearch",
crate::schema::SearchBackend::Meilisearch => "Meilisearch",
crate::schema::SearchBackend::Postgres => {
unreachable!("Postgres-backed search indexes are never collected into SearchEnqueueInfo")
}
}
}
fn enqueue_search_cte_sql(eq: &SearchEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
let index_name_sql = match &eq.index_name {
None => "NULL".to_string(),
Some(name) => sql_str(name),
};
format!(
concat!(
"\"{}\" AS (\n",
" INSERT INTO _pylon.\"IndexOutbox\"\n",
" (object_id, type_name, index_kind, index_name, operation)\n",
" SELECT \"id\", {}, '{}'::_pylon.\"IndexKind\", {}, {}\n",
" FROM \"{}\"\n",
" ON CONFLICT (object_id, index_kind, index_name)\n",
" DO UPDATE SET status = 'Pending', operation = EXCLUDED.operation, enqueued_at = now()\n",
")",
),
cte_name,
sql_str(&eq.type_name),
search_backend_index_kind(&eq.backend),
index_name_sql,
sql_str(eq.operation),
source_cte,
)
}
fn enqueue_search_ctes(enqueue: &[SearchEnqueueInfo], source_cte: &str, offset: usize) -> Vec<String> {
enqueue
.iter()
.enumerate()
.map(|(i, eq)| enqueue_search_cte_sql(eq, source_cte, &format!("_es{}", offset + i)))
.collect()
}
fn shape_select_from_cte(
target: &IrSource,
returning: &[IrShapePointer],
cte_name: &str,
) -> (ShapeDescriptor, Option<String>) {
if returning.is_empty() {
return (
ShapeDescriptor {
root: ShapeNode::Scalar {
name: String::new(),
position: 0,
},
},
None,
);
}
let (pointer_exprs, shape_pointers) = build_shape(returning, "");
let mut parts = vec![type_disc(&target.type_name)];
parts.extend(pointer_exprs);
let tuple = parts.join(",\n ");
let sql = format!("SELECT (\n {}\n) AS result\nFROM {}", tuple, qi(cte_name));
let root_pointers = prepend_type(shape_pointers);
let shape = ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: Some(target.type_name.clone()),
position: 0,
cardinality: Cardinality::Required,
pointers: root_pointers,
has_implicit_id: shape_has_implicit_id(returning),
},
};
(shape, Some(sql))
}
fn emit_insert_row_sql(ins: &IrInsert) -> String {
let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
let cols: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(c, _)| qi(c))
.chain(ins.rewrites.iter().map(|r| qi(&r.column)))
.collect();
let vals: Vec<String> = ins
.assignments
.iter()
.filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
.map(|(_, e)| emit_expr(e))
.chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
.collect();
if let Some(guard) = &ins.guard {
let from_ctes = nested_cte_from(&ins.nested_ctes, &vals.join(","), " ");
let projection = if cols.is_empty() {
String::new()
} else {
format!(" {}", vals.join(", "))
};
let column_list = if cols.is_empty() {
String::new()
} else {
format!(" ({})", cols.join(", "))
};
return format!(
"INSERT INTO {}{} SELECT{}{} WHERE {}",
target_ref(&ins.target),
column_list,
projection,
from_ctes,
emit_expr(guard),
);
}
if cols.is_empty() && ins.nested_ctes.is_empty() {
format!("INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
} else if ins.nested_ctes.is_empty() {
format!(
"INSERT INTO {} ({}) VALUES ({})",
target_ref(&ins.target),
cols.join(", "),
vals.join(", ")
)
} else {
format!(
"INSERT INTO {} ({}) SELECT {}{}",
target_ref(&ins.target),
cols.join(", "),
vals.join(", "),
nested_cte_from(&ins.nested_ctes, &vals.join(","), " "),
)
}
}
fn emit_insert_stmt(ins: &IrInsert) -> SqlOutput {
if ins.enqueue_vector.is_empty()
&& ins.enqueue_search.is_empty()
&& !insert_has_any_multilink(ins)
&& ins.nested_ctes.is_empty()
{
let mut sql = emit_insert_row_sql(ins);
if let Some(conflict) = &ins.unless_conflict {
emit_conflict(&mut sql, conflict);
}
let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
if let Some(r) = returning_sql {
sql.push_str(&r);
}
return SqlOutput {
sql,
shape,
inference_plan: None,
};
}
let mut cte_parts = if insert_has_any_multilink(ins) {
emit_insert_multilink_ctes(ins, "_w")
} else {
let mut cte_parts = emit_user_cte_parts(&ins.nested_ctes);
let mut insert_sql = emit_insert_row_sql(ins);
if let Some(conflict) = &ins.unless_conflict {
emit_conflict(&mut insert_sql, conflict);
}
insert_sql.push_str("\nRETURNING \"id\"");
cte_parts.push(format!("\"_w\" AS (\n{}\n)", insert_sql));
cte_parts
};
cte_parts.extend(enqueue_ctes(&ins.enqueue_vector, "_w"));
cte_parts.extend(enqueue_search_ctes(&ins.enqueue_search, "_w", ins.enqueue_vector.len()));
let (shape, select_sql) = shape_select_from_cte(&ins.target, &ins.returning, "_w");
let sql = format!(
"WITH\n{}\n{}",
cte_parts.join(",\n"),
select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
);
SqlOutput {
sql,
shape,
inference_plan: None,
}
}
fn emit_poly_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
let alias = &upd.target.alias;
let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
let mut union_parts = vec![];
let from_ctes = nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n");
let has_any_multilink = !upd.multi_link_clears.is_empty()
|| !upd.multi_link_replaces.is_empty()
|| !upd.multi_link_appends.is_empty()
|| !upd.multi_link_removals.is_empty();
for (i, imp) in upd.poly_implementors.iter().enumerate() {
let cte_name = format!("_u{}", i);
if has_any_multilink {
cte_parts.extend(emit_update_multilink_cte_parts(
&concrete_poly_update(upd, imp),
&cte_name,
false,
));
} else {
let mut upd_sql = format!(
"UPDATE {} AS {}\nSET {}{}",
qn(&imp.module, &imp.table),
qi(alias),
sets.join(", "),
from_ctes,
);
append_filter(&mut upd_sql, &upd.filter);
upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
}
let r_alias = format!("_r{}", i);
union_parts.push(format!(
"SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
sql_str(&imp.type_name),
qi(&r_alias),
cte_name,
qi(&r_alias),
));
}
let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
SqlOutput {
sql,
shape,
inference_plan: None,
}
}
fn emit_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
if !upd.poly_implementors.is_empty() {
return emit_poly_update_stmt(upd, user_ctes);
}
let alias = &upd.target.alias;
let (shape, returning_sql) = emit_returning_shape(&upd.target, &upd.returning, true);
let has_any_multilink = !upd.multi_link_clears.is_empty()
|| !upd.multi_link_replaces.is_empty()
|| !upd.multi_link_appends.is_empty()
|| !upd.multi_link_removals.is_empty();
if !has_any_multilink && upd.enqueue_vector.is_empty() && upd.enqueue_search.is_empty() {
let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
let mut sql = format!(
"UPDATE {} AS {}\nSET {}",
target_ref(&upd.target),
qi(alias),
sets.join(", "),
);
sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
append_filter(&mut sql, &upd.filter);
if let Some(r) = returning_sql {
sql.push_str(&r);
}
let combined_ctes: Vec<IrCteDef> = user_ctes
.iter()
.cloned()
.chain(upd.nested_ctes.iter().cloned())
.collect();
if !combined_ctes.is_empty() {
sql = format!("{}{}", emit_cte_prefix(&combined_ctes), sql);
}
return SqlOutput {
sql,
shape,
inference_plan: None,
};
}
if !has_any_multilink && (!upd.enqueue_vector.is_empty() || !upd.enqueue_search.is_empty()) {
let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
let mut upd_sql = format!(
" UPDATE {} AS {}\n SET {}",
target_ref(&upd.target),
qi(alias),
sets.join(", "),
);
upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
append_filter(&mut upd_sql, &upd.filter);
upd_sql.push_str("\n RETURNING \"id\"");
let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
cte_parts.push(format!("\"_w\" AS (\n{}\n)", upd_sql));
cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_w"));
cte_parts.extend(enqueue_search_ctes(&upd.enqueue_search, "_w", upd.enqueue_vector.len()));
let (shape2, select_sql) = shape_select_from_cte(&upd.target, &upd.returning, "_w");
let sql = format!(
"WITH\n{}\n{}",
cte_parts.join(",\n"),
select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
);
return SqlOutput {
sql,
shape: shape2,
inference_plan: None,
};
}
let result_expr = if !upd.returning.is_empty() {
let (pointer_exprs, _) = build_shape(&upd.returning, alias);
let mut parts = vec![type_disc(&upd.target.type_name)];
parts.extend(pointer_exprs);
parts.join(",\n ")
} else {
format!("{}.id", qi(alias))
};
let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
if has_scalar_changes {
let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
let mut upd_sql = format!(
"UPDATE {} AS {}\nSET {}",
target_ref(&upd.target),
qi(alias),
sets.join(", "),
);
upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
append_filter(&mut upd_sql, &upd.filter);
upd_sql.push_str("\nRETURNING *");
cte_parts.push(format!("\"_ids\" AS (\n{}\n)", upd_sql));
} else {
let mut sel = format!(
"SELECT {}.* FROM {} AS {}",
qi(alias),
target_ref(&upd.target),
qi(alias),
);
append_filter(&mut sel, &upd.filter);
cte_parts.push(format!("\"_ids\" AS (\n{}\n)", sel));
}
for (i, clr) in upd.multi_link_clears.iter().enumerate() {
let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
let del = format!(
"DELETE FROM {} WHERE {} IN (SELECT id FROM \"_ids\"){}",
qn(&clr.module, &clr.junction_table),
qi(&clr.source_col),
exclude,
);
cte_parts.push(format!("\"_clr_{}\" AS (\n{}\n)", i, del));
}
for (i, app) in upd.multi_link_appends.iter().enumerate() {
cte_parts.push(emit_ml_append_cte(
app,
"_ids",
&upd.target.alias,
&format!("_ml_add_{}", i),
));
}
for (i, rem) in upd.multi_link_removals.iter().enumerate() {
cte_parts.push(emit_ml_remove_cte(
rem,
"_ids",
&upd.target.alias,
&format!("_ml_rm_{}", i),
));
}
for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
cte_parts.push(emit_ml_append_cte(
rep,
"_ids",
&upd.target.alias,
&format!("_ml_rep_{}", i),
));
}
cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_ids"));
cte_parts.extend(enqueue_search_ctes(
&upd.enqueue_search,
"_ids",
upd.enqueue_vector.len(),
));
let sql = format!(
"WITH\n{}\nSELECT (\n {}\n) AS result\nFROM \"_ids\" AS {}",
cte_parts.join(",\n"),
result_expr,
qi(alias),
);
SqlOutput {
sql,
shape,
inference_plan: None,
}
}
fn emit_delete_stmt(del: &IrDelete) -> SqlOutput {
if !del.poly_implementors.is_empty() {
return emit_poly_delete_stmt(del);
}
let alias = &del.target.alias;
if del.enqueue_search.is_empty() {
let mut sql = format!("DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
append_filter(&mut sql, &del.filter);
let (shape, returning_sql) = emit_returning_shape(&del.target, &del.returning, true);
if let Some(r) = returning_sql {
sql.push_str(&r);
}
return SqlOutput {
sql,
shape,
inference_plan: None,
};
}
let mut del_sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
append_filter(&mut del_sql, &del.filter);
del_sql.push_str("\n RETURNING \"id\"");
let mut cte_parts = vec![format!("\"_del\" AS (\n{}\n)", del_sql)];
cte_parts.extend(enqueue_search_ctes(&del.enqueue_search, "_del", 0));
let (shape, select_sql) = shape_select_from_cte(&del.target, &del.returning, "_del");
let sql = format!(
"WITH\n{}\n{}",
cte_parts.join(",\n"),
select_sql.unwrap_or_else(|| "SELECT * FROM \"_del\"".to_string()),
);
SqlOutput {
sql,
shape,
inference_plan: None,
}
}
fn emit_poly_delete_stmt(del: &IrDelete) -> SqlOutput {
let alias = &del.target.alias;
let mut cte_parts = vec![];
let mut union_parts = vec![];
for (i, imp) in del.poly_implementors.iter().enumerate() {
let cte_name = format!("_d{}", i);
let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
append_filter(&mut del_sql, &del.filter);
del_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
let r_alias = format!("_r{}", i);
union_parts.push(format!(
"SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
sql_str(&imp.type_name),
qi(&r_alias),
cte_name,
qi(&r_alias),
));
}
let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
let (shape, _) = emit_returning_shape(&del.target, &del.returning, true);
SqlOutput {
sql,
shape,
inference_plan: None,
}
}
fn emit_returning_shape(
target: &IrSource,
returning: &[IrShapePointer],
with_alias: bool,
) -> (ShapeDescriptor, Option<String>) {
if returning.is_empty() {
return (
ShapeDescriptor {
root: ShapeNode::Scalar {
name: String::new(),
position: 0,
},
},
None,
);
}
let alias = if with_alias { target.alias.as_str() } else { "" };
let (pointer_exprs, shape_pointers) = build_shape(returning, alias);
let mut parts = vec![type_disc(&target.type_name)];
parts.extend(pointer_exprs);
let tuple = parts.join(",\n ");
let sql = format!("\nRETURNING (\n {}\n) AS result", tuple);
let root_pointers = prepend_type(shape_pointers);
let shape = ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: Some(target.type_name.clone()),
position: 0,
cardinality: Cardinality::Required,
pointers: root_pointers,
has_implicit_id: shape_has_implicit_id(returning),
},
};
(shape, Some(sql))
}
fn emit_scalar_set(f: &IrScalarSetPointer, pos: usize) -> (String, ShapeNode) {
let from_sql = if !f.poly_implementors.is_empty() {
format!(
"(\n{}\n) AS {}",
emit_poly_union(&f.poly_implementors, &f.poly_columns),
qi(&f.source.alias)
)
} else {
format!("{} AS {}", source_ref(&f.source), qi(&f.source.alias))
};
let sql = format!(
"(SELECT COALESCE(array_agg(ROW({})::record), ARRAY[]::record[]) FROM {})",
emit_expr(&f.bool_expr),
from_sql,
);
let node = ShapeNode::Array {
name: f.alias.clone(),
position: pos,
element: Box::new(ShapeNode::Scalar {
name: String::new(),
position: 0,
}),
};
(sql, node)
}
fn shape_has_implicit_id(pointers: &[IrShapePointer]) -> bool {
matches!(pointers.first(), Some(IrShapePointer::Scalar(p)) if p.implicit_id)
}
fn build_shape(pointers: &[IrShapePointer], table_alias: &str) -> (Vec<String>, Vec<ShapeNode>) {
let mut exprs = Vec::new();
let mut nodes = Vec::new();
for (i, pointer) in pointers.iter().enumerate() {
let (sql, node) = emit_shape_pointer(pointer, table_alias, i + 1);
exprs.push(sql);
nodes.push(node);
}
(exprs, nodes)
}
fn fill_template(template: &str, args: &[String]) -> String {
let mut out = String::with_capacity(template.len());
let mut chars = template.char_indices().peekable();
while let Some((start, c)) = chars.next() {
if c != '$' {
out.push(c);
continue;
}
let mut end = start + 1;
while let Some((i, d)) = chars.peek().copied()
&& d.is_ascii_digit()
{
end = i + d.len_utf8();
chars.next();
}
match template[start + 1..end].parse::<usize>() {
Ok(n) if (1..=args.len()).contains(&n) => out.push_str(&args[n - 1]),
_ => out.push_str(&template[start..end]),
}
}
out
}
fn assert_message_arg(message: &Option<IrExpr>) -> String {
message
.as_ref()
.map(|m| format!(", ({})::text", emit_expr(m)))
.unwrap_or_default()
}
fn emit_shape_pointer(pointer: &IrShapePointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
match pointer {
IrShapePointer::Scalar(f) => emit_scalar(f, table_alias, pos),
IrShapePointer::SingleLink(f) => emit_single_link(f, table_alias, pos),
IrShapePointer::MultiLink(f) => emit_multi_link(f, table_alias, pos),
IrShapePointer::Computed(f) => (emit_expr(&f.expr), expr_shape_node(&f.alias, pos, &f.expr)),
IrShapePointer::ScalarSet(f) => emit_scalar_set(f, pos),
IrShapePointer::Asserted(a) => {
let (sql, node) = emit_shape_pointer(&a.inner, table_alias, pos);
let checked_set = match &a.check {
Some(check) if emits_one_object(check) => {
format!("ARRAY{}", emit_shape_pointer(check, table_alias, pos).0)
}
Some(check) => emit_shape_pointer(check, table_alias, pos).0,
None if emits_one_object(&a.inner) => format!("ARRAY{sql}"),
None => "\"_a\".\"v\"".to_string(),
};
let checked = format!(
"(SELECT \"_a\".\"v\" FROM (SELECT {sql} AS \"v\") AS \"_a\"\n WHERE cardinality(\"_pylon\".{}({checked_set}::text[]{})) >= 0)",
qi(&a.fn_name),
assert_message_arg(&a.message),
);
(checked, node)
}
}
}
fn emits_one_object(pointer: &IrShapePointer) -> bool {
match pointer {
IrShapePointer::SingleLink(_) => true,
IrShapePointer::Computed(c) => matches!(
c.expr,
IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { multi: false, .. }
),
IrShapePointer::Asserted(a) => emits_one_object(&a.inner),
_ => false,
}
}
fn pg_quoted_to_pylon(pg_type: &str) -> String {
let inner = pg_type.trim_start_matches('"');
if let Some(idx) = inner.find(r#""."#) {
let module = &inner[..idx];
let type_name = inner[idx + 3..].trim_end_matches('"');
format!("{}::{}", module, type_name)
} else {
pg_type.to_string()
}
}
struct QualifiedPgType {
name: String,
is_array: bool,
}
impl QualifiedPgType {
fn of(pg_type: &str) -> Option<Self> {
let (element, is_array) = match pg_type.strip_suffix("[]") {
Some(element) => (element, true),
None => (pg_type, false),
};
element.starts_with('"').then(|| Self {
name: pg_quoted_to_pylon(element),
is_array,
})
}
fn text_cast(&self) -> &'static str {
if self.is_array { "::text[]" } else { "::text" }
}
fn shape_node(&self, name: String, position: usize) -> ShapeNode {
let element = ShapeNode::Enum {
name: name.clone(),
position,
enum_type: self.name.clone(),
};
if !self.is_array {
return element;
}
ShapeNode::Array {
name,
element: Box::new(ShapeNode::Enum {
name: String::new(),
position: 0,
enum_type: self.name.clone(),
}),
position,
}
}
}
fn emit_scalar(f: &IrScalarPointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
if let Some(nt_name) = f.pg_type.strip_prefix("__nt__:") {
let sql = if table_alias.is_empty() {
format!("{}::jsonb", qi(&f.column))
} else {
format!("{}.{}::jsonb", qi(table_alias), qi(&f.column))
};
return (
sql,
ShapeNode::NamedTuple {
name: f.alias.clone(),
position: pos,
type_name: Some(nt_name.to_string()),
members: f.tuple_shape.as_ref().map(|s| s.members.clone()),
is_free_object: false,
},
);
}
if let Some(qualified) = QualifiedPgType::of(&f.pg_type) {
let cast = qualified.text_cast();
let sql = if table_alias.is_empty() {
format!("{}{}", qi(&f.column), cast)
} else {
format!("{}.{}{}", qi(table_alias), qi(&f.column), cast)
};
return (sql, qualified.shape_node(f.alias.clone(), pos));
}
if let Some(shape) = &f.tuple_shape {
let sql = if table_alias.is_empty() {
format!("{}::jsonb", qi(&f.column))
} else {
format!("{}.{}::jsonb", qi(table_alias), qi(&f.column))
};
return (
sql,
ShapeNode::NamedTuple {
name: f.alias.clone(),
position: pos,
type_name: shape.type_name.clone(),
members: Some(shape.members.clone()),
is_free_object: false,
},
);
}
let sql = if table_alias.is_empty() {
format!("{}::{}", qi(&f.column), f.pg_type)
} else {
format!("{}.{}::{}", qi(table_alias), qi(&f.column), f.pg_type)
};
(
sql,
ShapeNode::Scalar {
name: f.alias.clone(),
position: pos,
},
)
}
fn emit_single_link(f: &IrSingleLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
let sub = &f.subquery;
let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
unreachable!("single-link subquery is always schema-bound")
};
let sub_alias = &source.alias;
let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
let mut parts = vec![source_type_disc(source)];
parts.extend(sub_exprs);
for lp in &f.link_properties {
parts.push(format!("\"jt\".{}", qi(&lp.name)));
let pos = sub_nodes.len() + 1;
sub_nodes.push(ShapeNode::Scalar {
name: format!("@{}", lp.name),
position: pos,
});
}
let tuple = parts.join(",\n ");
let (from_sql, mut where_parts) = match &f.correlation {
IrSingleLinkCorrelation::Fk { fk_column, target_pk } => {
let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
let cond = format!(
"{}.{} = {}.{}",
qi(parent_alias),
qi(fk_column),
qi(sub_alias),
qi(target_pk),
);
(from, vec![cond])
}
IrSingleLinkCorrelation::Junction { join, target_pk: _ } => match join {
IrMultiLinkJoin::Standard { junction_table, module } => {
let from = format!(
"FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
junction_ref(module, junction_table),
source_ref(source),
qi(sub_alias),
qi(sub_alias),
);
let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
(from, vec![cond])
}
IrMultiLinkJoin::Through {
junction_table,
module,
source_col,
target_col,
} => {
let from = format!(
"FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
junction_ref(module, junction_table),
source_ref(source),
qi(sub_alias),
qi(sub_alias),
qi(target_col),
);
let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
(from, vec![cond])
}
IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
unreachable!("a junction-backed single link's own forward join is always Standard or Through")
}
},
};
if let Some(filter) = &sub.filter {
where_parts.push(emit_expr(filter));
}
let mut sql = format!(
"(SELECT (\n {}\n )\n {}\n WHERE {}",
tuple,
from_sql,
where_parts.join(" AND "),
);
if !sub.order_by.is_empty() {
let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
sql.push_str(&format!("\n ORDER BY {}", s.join(", ")));
}
sql.push(')');
let node = ShapeNode::Object {
name: f.alias.clone(),
type_name: Some(source.type_name.clone()),
position: pos,
cardinality: Cardinality::Optional,
pointers: prepend_type(sub_nodes),
has_implicit_id: shape_has_implicit_id(shape),
};
(sql, node)
}
fn emit_multi_link(f: &IrMultiLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
let sub = &f.subquery;
let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
unreachable!("multi-link subquery is always schema-bound")
};
let sub_alias = &source.alias;
let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
let mut row_parts = vec![source_type_disc(source)];
row_parts.extend(sub_exprs);
for lp in &f.link_properties {
row_parts.push(format!("\"jt\".{}", qi(&lp.name)));
let pos = sub_nodes.len() + 1;
sub_nodes.push(ShapeNode::Scalar {
name: format!("@{}", lp.name),
position: pos,
});
}
let row = row_parts.join(",\n ");
let order_sql = if !sub.order_by.is_empty() {
let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
format!(" ORDER BY {}", s.join(", "))
} else {
String::new()
};
let (from_sql, source_cond) = match &f.join {
IrMultiLinkJoin::Standard { junction_table, module } => {
let from = format!(
"FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
junction_ref(module, junction_table),
source_ref(source),
qi(sub_alias),
qi(sub_alias),
);
let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
(from, cond)
}
IrMultiLinkJoin::Through {
junction_table,
module,
source_col,
target_col,
} => {
let from = format!(
"FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
junction_ref(module, junction_table),
source_ref(source),
qi(sub_alias),
qi(sub_alias),
qi(target_col),
);
let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
(from, cond)
}
IrMultiLinkJoin::BacklinkFk { fk_col } => {
let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
let cond = format!("{}.{} = {}.id", qi(sub_alias), qi(fk_col), qi(parent_alias));
(from, cond)
}
IrMultiLinkJoin::BacklinkJunction {
junction_table,
module,
owner_col,
current_col,
} => {
let from = format!(
"FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
junction_ref(module, junction_table),
source_ref(source),
qi(sub_alias),
qi(sub_alias),
qi(owner_col),
);
let cond = format!("\"jt\".{} = {}.id", qi(current_col), qi(parent_alias));
(from, cond)
}
};
let mut where_parts = vec![source_cond];
if let Some(filter) = &sub.filter {
where_parts.push(emit_expr(filter));
}
if f.single {
let mut sql = format!(
"(SELECT (\n {}\n )\n {}\n WHERE {}",
row,
from_sql,
where_parts.join(" AND "),
);
append_order_by(&mut sql, &sub.order_by);
append_offset_limit(&mut sql, &sub.offset, &sub.limit);
sql.push(')');
let node = ShapeNode::Object {
name: f.alias.clone(),
type_name: Some(source.type_name.clone()),
position: pos,
cardinality: Cardinality::Optional,
pointers: prepend_type(sub_nodes),
has_implicit_id: shape_has_implicit_id(shape),
};
return (sql, node);
}
if f.single {
let mut sql = format!(
"(SELECT (\n {}\n )\n {}\n WHERE {}",
row,
from_sql,
where_parts.join(" AND "),
);
append_order_by(&mut sql, &sub.order_by);
append_offset_limit(&mut sql, &sub.offset, &sub.limit);
sql.push(')');
let node = ShapeNode::Object {
name: f.alias.clone(),
type_name: Some(source.type_name.clone()),
position: pos,
cardinality: Cardinality::Optional,
pointers: prepend_type(sub_nodes),
has_implicit_id: shape_has_implicit_id(shape),
};
return (sql, node);
}
let sql = if sub.limit.is_some() || sub.offset.is_some() {
let mut cols: Vec<String> = row_parts
.iter()
.enumerate()
.map(|(i, part)| format!("{} AS \"c{}\"", part, i))
.collect();
let agg_row: Vec<String> = (0..row_parts.len()).map(|i| format!("\"__lim\".\"c{}\"", i)).collect();
let mut agg_order: Vec<String> = vec![];
for (i, s) in sub.order_by.iter().enumerate() {
cols.push(format!("{} AS \"s{}\"", emit_expr(&s.expr), i));
agg_order.push(emit_sort_clause(&IrSort {
expr: IrExpr::ColumnRef {
alias: "__lim".to_string(),
column: format!("s{}", i),
pg_type: String::new(),
},
direction: s.direction.clone(),
nulls: s.nulls.clone(),
}));
}
let mut inner = format!(
"SELECT {}\n {}\n WHERE {}",
cols.join(",\n "),
from_sql,
where_parts.join(" AND "),
);
append_order_by(&mut inner, &sub.order_by);
append_offset_limit(&mut inner, &sub.offset, &sub.limit);
let agg_order_sql = if agg_order.is_empty() {
String::new()
} else {
format!(" ORDER BY {}", agg_order.join(", "))
};
format!(
"(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n FROM ({}) AS \"__lim\")",
agg_row.join(",\n "),
agg_order_sql,
inner,
)
} else {
format!(
"(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n {}\n WHERE {})",
row,
order_sql,
from_sql,
where_parts.join(" AND "),
)
};
let node = ShapeNode::Array {
name: f.alias.clone(),
position: pos,
element: Box::new(ShapeNode::Object {
name: String::new(),
type_name: Some(source.type_name.clone()),
position: 0,
cardinality: Cardinality::Required,
pointers: prepend_type(sub_nodes),
has_implicit_id: shape_has_implicit_id(shape),
}),
};
(sql, node)
}
fn prepend_type(nodes: Vec<ShapeNode>) -> Vec<ShapeNode> {
let mut out = vec![ShapeNode::Scalar {
name: "__type__".into(),
position: 0,
}];
out.extend(nodes);
out
}
fn append_filter(sql: &mut String, filter: &Option<IrExpr>) {
if let Some(f) = filter {
sql.push_str(&format!("\nWHERE {}", emit_expr(f)));
}
}
fn append_order_by(sql: &mut String, order_by: &[IrSort]) {
if !order_by.is_empty() {
let s: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
sql.push_str(&format!("\nORDER BY {}", s.join(", ")));
}
}
fn append_offset_limit(sql: &mut String, offset: &Option<IrExpr>, limit: &Option<IrExpr>) {
if let Some(o) = offset {
sql.push_str(&format!("\nOFFSET {}", emit_expr(o)));
}
if let Some(l) = limit {
sql.push_str(&format!("\nLIMIT {}", emit_expr(l)));
}
}
fn append_lock_clause(sql: &mut String, lock: &Option<IrLockClause>) {
let Some(lock) = lock else { return };
let strength = match lock.strength {
IrLockStrength::Update => "UPDATE",
IrLockStrength::NoKeyUpdate => "NO KEY UPDATE",
IrLockStrength::Share => "SHARE",
IrLockStrength::KeyShare => "KEY SHARE",
};
sql.push_str(&format!("\nFOR {}", strength));
match lock.wait {
IrLockWait::Block => {}
IrLockWait::NoWait => sql.push_str(" NOWAIT"),
IrLockWait::SkipLocked => sql.push_str(" SKIP LOCKED"),
}
}
fn emit_sort_clause(s: &IrSort) -> String {
let dir = match s.direction {
IrSortDir::Asc => "ASC",
IrSortDir::Desc => "DESC",
};
let nulls = match s.nulls {
IrNulls::First => "NULLS FIRST",
IrNulls::Last => "NULLS LAST",
};
format!("{} {} {}", emit_expr(&s.expr), dir, nulls)
}
fn set_operand(expr: &IrExpr) -> String {
match expr {
IrExpr::ArrayFromSelect(_) => format!("SELECT unnest({})", emit_expr(expr)),
IrExpr::CteRef { name, scalar, .. } => {
format!("SELECT {} FROM {}", if *scalar { "\"v\"" } else { "\"id\"" }, qi(name))
}
_ => format!("SELECT {}", emit_expr(expr)),
}
}
pub fn emit_expr_with_fanouts(expr: &IrExpr, fanouts: &HashMap<(String, String), IrPolyFanout>) -> String {
with_subtype_fanouts(fanouts, || emit_expr(expr))
}
pub fn emit_expr(expr: &IrExpr) -> String {
match expr {
IrExpr::ColumnRef { alias, column, .. } => {
if alias.is_empty() {
qi(column)
} else if alias == "NEW" || alias == "OLD" {
format!("{alias}.{}", qi(column))
} else {
format!("{}.{}", qi(alias), qi(column))
}
}
IrExpr::Param { index } => format!("${}", index + 1),
IrExpr::Literal(lit) => emit_literal(lit),
IrExpr::BinOp(op) => {
let l = emit_expr(&op.left);
let r = emit_expr(&op.right);
match op.op {
BinOpKind::Add => format!("({} + {})", l, r),
BinOpKind::Sub => format!("({} - {})", l, r),
BinOpKind::Mul => format!("({} * {})", l, r),
BinOpKind::Div => format!("({} / {})", l, r),
BinOpKind::FloorDiv => {
if is_integer_expr(&op.left) && is_integer_expr(&op.right) {
format!("({} / {})", l, r)
} else {
format!("floor(({}) / ({}))", l, r)
}
}
BinOpKind::Mod => format!("({} % {})", l, r),
BinOpKind::Pow => format!("power({}, {})", l, r),
BinOpKind::Eq => format!("({} = {})", l, r),
BinOpKind::Ne => format!("({} <> {})", l, r),
BinOpKind::Lt => format!("({} < {})", l, r),
BinOpKind::Le => format!("({} <= {})", l, r),
BinOpKind::Gt => format!("({} > {})", l, r),
BinOpKind::Ge => format!("({} >= {})", l, r),
BinOpKind::And => format!("({} AND {})", l, r),
BinOpKind::Or => format!("({} OR {})", l, r),
BinOpKind::Like => format!("({} LIKE {})", l, r),
BinOpKind::Ilike => format!("({} ILIKE {})", l, r),
BinOpKind::NotLike => format!("({} NOT LIKE {})", l, r),
BinOpKind::NotIlike => format!("({} NOT ILIKE {})", l, r),
BinOpKind::In => format!("({} = ANY({}))", l, unwrap_unnest_for_any(&op.right, &r)),
BinOpKind::NotIn => format!("({} <> ALL({}))", l, unwrap_unnest_for_any(&op.right, &r)),
BinOpKind::Coalesce => format!("COALESCE({}, {})", l, r),
BinOpKind::CoalesceEq => format!("({} IS NOT DISTINCT FROM {})", l, r),
BinOpKind::CoalesceNe => format!("({} IS DISTINCT FROM {})", l, r),
BinOpKind::Concat => format!("({} || {})", l, r),
}
}
IrExpr::UnaryOp(op) => {
let inner = emit_expr(&op.operand);
match op.op {
UnaryOpKind::Not => format!("(NOT {})", inner),
UnaryOpKind::Minus => format!("(-{})", inner),
UnaryOpKind::Exists => format!("EXISTS({})", inner),
UnaryOpKind::Distinct => inner,
}
}
IrExpr::FunctionCall(f) => {
let args: Vec<_> = f.args.iter().map(emit_expr).collect();
if let Some(tmpl) = &f.sql_template {
return fill_template(tmpl, &args);
}
let name = match &f.schema {
Some(s) => format!("{}.{}", pg_schema(s), qi(&f.name)),
None => f.name.clone(),
};
format!("{}({})", name, args.join(", "))
}
IrExpr::TypeCast(c) => {
if c.pg_type == "jsonb" {
match &c.expr {
IrExpr::Param { .. } => format!("({})::jsonb", emit_expr(&c.expr)),
IrExpr::Literal(IrLiteral::Str(_)) => {
format!("to_jsonb({}::text)", emit_expr(&c.expr))
}
_ => format!("to_jsonb({})", emit_expr(&c.expr)),
}
} else if yields_jsonb(&c.expr) {
format!("(({}) #>> '{{}}')::{}", emit_expr(&c.expr), c.pg_type)
} else {
format!("({})::{}", emit_expr(&c.expr), c.pg_type)
}
}
IrExpr::IfElse(ie) => format!(
"CASE WHEN {} THEN {} ELSE {} END",
emit_expr(&ie.condition),
emit_expr(&ie.if_),
emit_expr(&ie.else_),
),
IrExpr::Array(elems) => {
if elems.is_empty() {
"ARRAY[]::text[]".to_string()
} else {
let parts: Vec<String> = elems.iter().map(emit_expr).collect();
format!("ARRAY[{}]", parts.join(", "))
}
}
IrExpr::Null => "NULL".to_string(),
IrExpr::AggOverSet {
fn_name,
schema: _,
elems,
} => {
let union_all = elems
.iter()
.map(|e| format!("SELECT {}", emit_expr(e)))
.collect::<Vec<_>>()
.join(" UNION ALL ");
format!("(SELECT {}(v) FROM ({}) AS _set(v))", fn_name, union_all)
}
IrExpr::AggOverCte { fn_name, cte, column } => format!(
"(SELECT {}({}) FROM {})",
fn_name,
column.as_deref().map(qi).unwrap_or_else(|| "*".to_string()),
qi(cte),
),
IrExpr::ExistsOverCte { cte, column } => match column {
Some(column) => format!("EXISTS(SELECT 1 FROM {} WHERE {} IS NOT NULL)", qi(cte), qi(column)),
None => format!("EXISTS(SELECT 1 FROM {})", qi(cte)),
},
IrExpr::AggOverQuery { fn_name, inner } => {
let inner_sql = emit_select_stmt(inner, &[]).sql;
format!("(SELECT {}(*) FROM ({}) _agg)", fn_name, inner_sql)
}
IrExpr::ArrayFromSelect(src) => emit_array_source(src),
IrExpr::ScalarSubquery(sel) => {
format!(
"(SELECT \"v\" FROM (\n{}\n) AS \"_scalar_sub\")",
emit_select_stmt(sel, &[]).sql
)
}
IrExpr::SetOp { op, left, right, mode } => {
let set = format!("({}) {} ({})", set_operand(left), op.sql(), set_operand(right));
match mode {
crate::ir::SetOpMode::Exists => format!("EXISTS({set})"),
crate::ir::SetOpMode::Array => format!("ARRAY({set})"),
crate::ir::SetOpMode::Aggregate(f) => {
format!("(SELECT {f}(\"_s\".\"v\") FROM ({set}) AS \"_s\"(\"v\"))")
}
}
}
IrExpr::CteRef { name, scalar, .. } => {
let col = if *scalar { "v" } else { "id" };
format!("(SELECT \"{}\" FROM \"{}\")", col, name)
}
IrExpr::CteFieldRef { name, field, .. } => {
format!("(SELECT {} FROM {})", qi(field), qi(name))
}
IrExpr::ForVar { name, .. } => format!("\"_for_{}\".\"v\"", name),
IrExpr::EnumLiteral { pg_type, variant } => {
format!("'{}'::{}", variant.replace('\'', "''"), pg_type)
}
IrExpr::GlobalParam { index, pg_type } => {
format!("(${}::{})", index + 1, pg_type)
}
IrExpr::GlobalRef { cte_name } => {
format!("(SELECT \"value\" FROM \"{}\")", cte_name)
}
IrExpr::NamedTuple { fields, .. } => {
let pairs: Vec<String> = fields
.iter()
.flat_map(|(k, v)| [format!("'{}'", k.replace('\'', "''")), emit_expr(v)])
.collect();
format!("jsonb_build_object({})", pairs.join(", "))
}
IrExpr::Tuple(elems) => {
let items: Vec<String> = elems.iter().map(emit_expr).collect();
format!("jsonb_build_array({})", items.join(", "))
}
IrExpr::Subscript { expr, index, is_array } => {
let e = emit_expr(expr);
let i = emit_expr(index);
if *is_array {
format!("_pylon.array_subscript({}, ({})::bigint)", e, i)
} else {
format!("_pylon.str_subscript({}, ({})::bigint)", e, i)
}
}
IrExpr::Slice {
expr,
lower,
upper,
is_array,
} => {
let e = emit_expr(expr);
if *is_array {
let lo = lower
.as_deref()
.map(|x| format!("({}) + 1", emit_expr(x)))
.unwrap_or_else(|| "1".to_string());
let hi = upper.as_deref().map(emit_expr).unwrap_or_default();
if hi.is_empty() {
format!("({})[{}:]", e, lo)
} else {
format!("({})[{}:{}]", e, lo, hi)
}
} else {
let start = lower
.as_deref()
.map(|x| format!("({}) + 1", emit_expr(x)))
.unwrap_or_else(|| "1".to_string());
match upper.as_deref() {
Some(hi_expr) => {
let lo_val = lower.as_deref().map(emit_expr).unwrap_or_else(|| "0".to_string());
format!(
"substr({}, {}, GREATEST(0, ({}) - ({})))",
e,
start,
emit_expr(hi_expr),
lo_val
)
}
None => format!("substr({}, {})", e, start),
}
}
}
IrExpr::JsonbField { expr, field } => {
format!("({}->{})", emit_expr(expr), sql_str(field))
}
IrExpr::JsonbIndex { expr, index } => {
format!("({}->{})", emit_expr(expr), index)
}
IrExpr::FnParam { name, .. } => qi(name),
IrExpr::FnSubquery(fs) => {
let scalar = match fs.shape.as_slice() {
[IrShapePointer::Computed(c)] => emit_expr(&c.expr),
_ => unreachable!("a function scalar subquery always projects exactly one computed pointer"),
};
let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
let mut sql = format!(
"(SELECT {}{}\nFROM {}.{}({}) AS {}",
if fs.distinct { "DISTINCT " } else { "" },
scalar,
pg_schema(&fs.fn_module),
qi(&fs.fn_name),
args_sql,
qi(&fs.alias),
);
append_filter(&mut sql, &fs.filter);
append_order_by(&mut sql, &fs.order_by);
append_offset_limit(&mut sql, &fs.offset, &fs.limit);
sql.push(')');
sql
}
IrExpr::PathSubquery(ps) => {
let scalar = match &ps.result {
IrPathResult::Scalar(e, _) => emit_expr(e),
IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
};
let from_sql = emit_path_joins(&ps.root, &ps.joins);
let mut sql = format!(
"(SELECT {}{}\nFROM {}",
if ps.distinct { "DISTINCT " } else { "" },
scalar,
from_sql
);
append_filter(&mut sql, &ps.filter);
append_order_by(&mut sql, &ps.order_by);
append_offset_limit(&mut sql, &ps.offset, &ps.limit);
sql.push(')');
sql
}
IrExpr::ObjectPathUnion { branches, limit, multi } => {
let arms: Vec<String> = branches
.iter()
.map(|ps| {
let IrPathResult::Object {
alias,
type_name,
shape,
} = &ps.result
else {
unreachable!("an object path union's branches always land on objects")
};
let (pointer_exprs, _) = build_shape(shape, alias);
let mut parts = vec![result_type_disc(ps, alias, type_name)];
parts.extend(pointer_exprs);
let mut sql = format!(
"SELECT (\n {}\n) AS \"r\"\nFROM {}",
parts.join(",\n "),
emit_path_joins(&ps.root, &ps.joins),
);
append_filter(&mut sql, &ps.filter);
sql
})
.collect();
let mut sql = format!(
"{}(SELECT \"r\" FROM (\n{}\n) AS \"_u\"",
if *multi { "ARRAY" } else { "" },
arms.join("\nUNION ALL\n")
);
if let Some(limit) = limit {
sql.push_str(&format!("\nLIMIT {}", emit_expr(limit)));
}
sql.push(')');
sql
}
IrExpr::ObjectPathSubquery(ps) => {
let IrPathResult::Object {
alias,
type_name,
shape,
} = &ps.result
else {
unreachable!("an object path subquery always lands on an object")
};
let (pointer_exprs, _) = build_shape(shape, alias);
let mut parts = vec![result_type_disc(ps, alias, type_name)];
parts.extend(pointer_exprs);
let mut sql = format!(
"(SELECT (\n {}\n)\nFROM {}",
parts.join(",\n "),
emit_path_joins(&ps.root, &ps.joins),
);
append_filter(&mut sql, &ps.filter);
append_order_by(&mut sql, &ps.order_by);
append_offset_limit(&mut sql, &ps.offset, &ps.limit);
sql.push(')');
sql
}
IrExpr::ObjectSubquery(sel) => {
let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
unreachable!("an object subquery is always schema-bound")
};
let alias = &source.alias;
let (sub_exprs, _) = build_shape(shape, alias);
let mut row_parts = vec![source_type_disc(source)];
row_parts.extend(sub_exprs);
let mut sql = format!(
"(SELECT (\n {}\n )\n FROM {} AS {}",
row_parts.join(",\n "),
source_ref(source),
qi(alias),
);
append_filter(&mut sql, &sel.filter);
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
sql.push(')');
sql
}
IrExpr::Subquery(sel) => {
let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
unreachable!("scalar/exists subquery is always schema-bound")
};
let alias = &source.alias;
let mut sql = if shape.is_empty() {
format!("(SELECT 1\nFROM {} AS {}", source_ref(source), qi(alias))
} else if let Some(c) = shape.iter().find_map(|f| {
if let IrShapePointer::Computed(c) = f {
Some(c)
} else {
None
}
}) {
format!(
"(SELECT {}\nFROM {} AS {}",
emit_expr(&c.expr),
source_ref(source),
qi(alias)
)
} else {
let pk_col = shape
.iter()
.find_map(|f| {
if let IrShapePointer::Scalar(s) = f {
Some(s.column.as_str())
} else {
None
}
})
.unwrap_or("id");
format!(
"(SELECT {}.{}\nFROM {} AS {}",
qi(alias),
qi(pk_col),
source_ref(source),
qi(alias),
)
};
append_filter(&mut sql, &sel.filter);
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
sql.push(')');
sql
}
IrExpr::RawSql(s) => format!("({s})"),
}
}
fn emit_vector_search(vs: &IrVectorSearch) -> SqlOutput {
let alias = &vs.source.alias;
let dist_sql = format!(
"{}.{} {} {}",
qi(alias),
qi(&vs.vector_col),
vs.distance_op,
emit_expr(&vs.query_expr),
);
let (obj_tuple, object_shape_nodes) = if vs.object_shape.is_empty() {
let type_expr = type_disc(&vs.source.type_name);
let id_expr = format!("{}.\"id\"", qi(alias));
let tuple = format!("{},\n {}", type_expr, id_expr);
let id_node = ShapeNode::Scalar {
name: "id".to_string(),
position: 1,
};
(tuple, vec![id_node])
} else {
let (pointer_exprs, shape_pointers) = build_shape(&vs.object_shape, alias);
let mut parts = vec![type_disc(&vs.source.type_name)];
parts.extend(pointer_exprs);
(parts.join(",\n "), prepend_type(shape_pointers))
};
let outer = format!(
"NULL::text,\n ROW(\n {}\n )::record,\n {}",
obj_tuple, dist_sql,
);
let mut sql = format!(
"SELECT (\n {}\n) AS result\nFROM {} AS {}",
outer,
source_ref(&vs.source),
qi(alias),
);
append_filter(&mut sql, &vs.filter);
if let Some(dir) = &vs.order_by_distance {
let dir_sql = match dir {
IrSortDir::Asc => "ASC",
IrSortDir::Desc => "DESC",
};
sql.push_str(&format!("\nORDER BY {} {}", dist_sql, dir_sql));
}
append_offset_limit(&mut sql, &vs.offset, &vs.limit);
let object_node = ShapeNode::Object {
name: "object".to_string(),
type_name: Some(vs.source.type_name.clone()),
position: 1,
cardinality: Cardinality::Many,
pointers: object_shape_nodes,
has_implicit_id: shape_has_implicit_id(&vs.object_shape),
};
let shape = ShapeDescriptor {
root: ShapeNode::VectorSearch {
object_position: 1,
distance_position: 2,
object_node: Box::new(object_node),
},
};
let inference_plan = vs.inference_model.as_ref().map(|model_name| InferencePlan::Embedding {
model_name: model_name.clone(),
type_name: vs.inference_type_name.clone().unwrap_or_default(),
index_name: vs.inference_index_name.clone().unwrap_or(None),
query_param_name: vs.inference_query_param_name.clone().unwrap_or_default(),
query_literal: vs.inference_query_literal.clone(),
});
SqlOutput {
sql,
shape,
inference_plan,
}
}
fn emit_fts_search(fs: &IrFtsSearch) -> SqlOutput {
use crate::schema::SearchBackend;
if fs.backend != SearchBackend::Postgres {
return emit_fts_search_deferred(fs);
}
let alias = &fs.source.alias;
let search_col = format!("{}.{}", qi(alias), qi(&fs.search_col));
let query_sql = emit_expr(&fs.query_expr);
let tsquery = format!("{}('english', {})", fs.tsquery_fn, query_sql);
let rank_sql = format!("ts_rank({}, {})", search_col, tsquery);
let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
let type_expr = type_disc(&fs.source.type_name);
let id_expr = format!("{}.\"id\"", qi(alias));
let tuple = format!("{},\n {}", type_expr, id_expr);
let id_node = ShapeNode::Scalar {
name: "id".to_string(),
position: 1,
};
(tuple, vec![id_node])
} else {
let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
let mut parts = vec![type_disc(&fs.source.type_name)];
parts.extend(pointer_exprs);
(parts.join(",\n "), prepend_type(shape_pointers))
};
let outer = format!(
"NULL::text,\n ROW(\n {}\n )::record,\n {}",
obj_tuple, rank_sql,
);
let mut sql = format!(
"SELECT (\n {}\n) AS result\nFROM {} AS {}\nWHERE {} @@ {}",
outer,
source_ref(&fs.source),
qi(alias),
search_col,
tsquery,
);
if let Some(f) = &fs.filter {
sql.push_str(&format!(" AND ({})", emit_expr(f)));
}
if let Some(dir) = &fs.order_by_rank {
let dir_sql = match dir {
IrSortDir::Asc => "ASC",
IrSortDir::Desc => "DESC",
};
sql.push_str(&format!("\nORDER BY {} {}", rank_sql, dir_sql));
}
append_offset_limit(&mut sql, &fs.offset, &fs.limit);
let object_node = ShapeNode::Object {
name: "object".to_string(),
type_name: Some(fs.source.type_name.clone()),
position: 1,
cardinality: Cardinality::Many,
pointers: object_shape_nodes,
has_implicit_id: shape_has_implicit_id(&fs.object_shape),
};
let shape = ShapeDescriptor {
root: ShapeNode::FtsSearch {
object_position: 1,
rank_position: 2,
object_node: Box::new(object_node),
},
};
SqlOutput {
sql,
shape,
inference_plan: None,
}
}
fn emit_fts_search_deferred(fs: &IrFtsSearch) -> SqlOutput {
let alias = &fs.source.alias;
let ids_idx = fs
.deferred_ids_param
.expect("deferred_ids_param must be set for deferred backend");
let scores_idx = fs
.deferred_scores_param
.expect("deferred_scores_param must be set for deferred backend");
let ids_param = format!("${}", ids_idx + 1);
let scores_param = format!("${}", scores_idx + 1);
let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
let type_expr = type_disc(&fs.source.type_name);
let id_expr = format!("{}.\"id\"", qi(alias));
let tuple = format!("{},\n {}", type_expr, id_expr);
let id_node = ShapeNode::Scalar {
name: "id".to_string(),
position: 1,
};
(tuple, vec![id_node])
} else {
let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
let mut parts = vec![type_disc(&fs.source.type_name)];
parts.extend(pointer_exprs);
(parts.join(",\n "), prepend_type(shape_pointers))
};
let outer = format!(
"NULL::text,\n ROW(\n {}\n )::record,\n \"_os\".\"score\"",
obj_tuple,
);
let mut sql = format!(
concat!(
"SELECT (\n {}\n) AS result\n",
"FROM {} AS {}\n",
"JOIN UNNEST({}::uuid[], {}::float8[]) AS \"_os\"(\"id\", \"score\")\n",
" ON \"_os\".\"id\" = {}.\"id\"",
),
outer,
source_ref(&fs.source),
qi(alias),
ids_param,
scores_param,
qi(alias),
);
if let Some(f) = &fs.filter {
sql.push_str(&format!("\nWHERE ({})", emit_expr(f)));
}
if let Some(dir) = &fs.order_by_rank {
let dir_sql = match dir {
IrSortDir::Asc => "ASC",
IrSortDir::Desc => "DESC",
};
sql.push_str(&format!("\nORDER BY \"_os\".\"score\" {}", dir_sql));
}
let size = fs.limit.as_ref().and_then(|lim| {
if let IrExpr::Literal(IrLiteral::Int(n)) = lim {
Some(*n as usize)
} else {
None
}
});
let object_node = ShapeNode::Object {
name: "object".to_string(),
type_name: Some(fs.source.type_name.clone()),
position: 1,
cardinality: Cardinality::Many,
pointers: object_shape_nodes,
has_implicit_id: shape_has_implicit_id(&fs.object_shape),
};
let shape = ShapeDescriptor {
root: ShapeNode::FtsSearch {
object_position: 1,
rank_position: 2,
object_node: Box::new(object_node),
},
};
let backend_str = match fs.backend {
crate::schema::SearchBackend::Meilisearch => "meilisearch",
_ => "opensearch",
};
let inference_plan = Some(InferencePlan::Search {
backend: backend_str.to_string(),
index_name: fs.deferred_index_name.clone().unwrap_or_default(),
query_param_name: fs.deferred_query_param_name.clone().unwrap_or_default(),
query_literal: fs.deferred_query_literal.clone(),
size,
});
SqlOutput {
sql,
shape,
inference_plan,
}
}
fn emit_function_select(sel: &IrFunctionSelect) -> SqlOutput {
let alias = &sel.alias;
let (pointer_exprs, shape_pointers) = build_shape(&sel.shape, alias);
let type_expr = if sel.polymorphic {
format!("{}.\"__type__\"", qi(alias))
} else {
type_disc(&sel.type_name)
};
let mut parts = vec![type_expr];
parts.extend(pointer_exprs);
let tuple = parts.join(",\n ");
let distinct = if sel.distinct { "DISTINCT " } else { "" };
let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
let from_clause = if sel.polymorphic {
format!("{} AS {}", fn_call, qi(alias))
} else {
format!("{} AS {}", fn_call, qi(alias))
};
let mut sql = format!("SELECT {}(\n {}\n) AS result\nFROM {}", distinct, tuple, from_clause,);
append_filter(&mut sql, &sel.filter);
append_order_by(&mut sql, &sel.order_by);
append_offset_limit(&mut sql, &sel.offset, &sel.limit);
let root_pointers = prepend_type(shape_pointers);
SqlOutput {
sql,
shape: ShapeDescriptor {
root: ShapeNode::Object {
name: String::new(),
type_name: Some(sel.type_name.clone()),
position: 0,
cardinality: Cardinality::Many,
pointers: root_pointers,
has_implicit_id: shape_has_implicit_id(&sel.shape),
},
},
inference_plan: None,
}
}
pub fn emit_fn_body(ir: &crate::ir::IrOutput) -> String {
with_subtype_fanouts(&ir.subtype_fanouts, || emit_output_fn_body(ir))
}
fn emit_output_fn_body(ir: &crate::ir::IrOutput) -> String {
let body = match &ir.stmt {
IrStmt::Select(sel) if matches!(sel.rows.as_slice(), [IrRowSource::Free(IrFreeExpr::Scalar(_))]) => {
let IrRowSource::Free(IrFreeExpr::Scalar(e)) = &sel.rows[0] else {
unreachable!()
};
format!("SELECT {}", emit_expr(e))
}
other => emit_dml_as_cte_source(other),
};
if ir.ctes.is_empty() {
body
} else {
let cte_prefix = emit_cte_prefix(&ir.ctes);
format!("{}{}", cte_prefix, body)
}
}
fn emit_literal(lit: &IrLiteral) -> String {
match lit {
IrLiteral::Str(s) => sql_str(s),
IrLiteral::Int(i) => i.to_string(),
IrLiteral::Float(f) => {
let s = f.to_string();
let s = if s.contains('.') || s.contains('e') {
s
} else {
format!("{}.0", s)
};
format!("({}::float8)", s)
}
IrLiteral::Bool(b) => {
if *b {
"TRUE".into()
} else {
"FALSE".into()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir;
use crate::parse;
use crate::schema::{
FunctionDescriptor, FunctionParamDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
NamedTupleDescriptor, PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
};
#[test]
fn a_template_argument_keeps_the_parameters_it_carries() {
let filled = fill_template("f($1, $2, $10)", &["($2)::int".to_string(), "b".to_string()]);
assert_eq!(filled, "f(($2)::int, b, $10)");
}
fn make_schema() -> SchemaDescriptor {
SchemaDescriptor {
types: vec![
TypeDescriptor {
name: "Person".into(),
module: "default".into(),
table: "Person".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![
PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("uuidv7()".into()),
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
},
PropertyDescriptor {
name: "name".into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
},
PropertyDescriptor {
name: "age".into(),
pg_type: "int8".into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
},
],
links: vec![LinkDescriptor {
name: "company".into(),
target: "default::Company".into(),
nullable: true,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
}],
multilinks: vec![MultiLinkDescriptor {
name: "posts".into(),
target: "default::Post".into(),
through: None,
nullable: false,
description: None,
default_pyql: None,
on_delete: vec![],
is_exclusive: false,
}],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Company".into(),
module: "default".into(),
table: "Company".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![PropertyDescriptor {
name: "name".into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
}],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Post".into(),
module: "default".into(),
table: "Post".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![PropertyDescriptor {
name: "title".into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
}],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
}
}
fn compile_and_emit(query: &str) -> SqlOutput {
let schema = make_schema();
compile_and_emit_with(query, &schema)
}
fn compile_and_emit_with(query: &str, schema: &SchemaDescriptor) -> SqlOutput {
let ast = parse::parse(query).expect("parse failed");
let ir = ir::compile(&ast, schema).expect("IR compile failed");
emit(&ir)
}
#[test]
fn a_nested_read_inside_a_with_opens_no_second_with() {
let out = compile_and_emit_with(
"WITH a := (SELECT Person FILTER .age > 1), b := (SELECT Person LIMIT 1) SELECT (SELECT (a UNION b))",
&make_schema(),
);
assert_eq!(out.sql.matches("WITH").count(), 1, "{}", out.sql);
}
#[test]
fn a_comparison_against_a_multi_row_with_binding_tests_membership() {
let out = compile_and_emit_with(
"WITH companies := (SELECT Company FILTER .name = 'x') SELECT Person FILTER .company = companies",
&make_schema(),
);
assert!(
out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
"{}",
out.sql
);
let out = compile_and_emit_with(
"WITH companies := (SELECT Company) SELECT Person FILTER any(.company = companies)",
&make_schema(),
);
assert!(
out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
"{}",
out.sql
);
let out = compile_and_emit_with(
"WITH companies := (SELECT Company) SELECT Person FILTER .company != companies",
&make_schema(),
);
assert!(out.sql.contains("NOT (\"t1\".\"company_id\" = ANY("), "{}", out.sql);
}
#[test]
fn a_comparison_against_a_single_row_with_binding_stays_a_scalar_read() {
let out = compile_and_emit_with(
"WITH company := (SELECT Company LIMIT 1) SELECT Person FILTER .company = company",
&make_schema(),
);
assert!(out.sql.contains("= (SELECT \"id\" FROM \"company\")"), "{}", out.sql);
}
#[test]
fn an_aggregate_over_a_backlink_counts_inside_a_subquery() {
let out = compile_and_emit_with("SELECT Post { n := count(.<posts) }", &make_schema());
assert!(
!out.sql.contains("count((SELECT"),
"an aggregate over a scalar subquery:\n{}",
out.sql
);
assert!(out.sql.contains("count("), "{}", out.sql);
}
#[test]
fn an_aggregate_over_a_relative_subselect_counts_inside_a_subquery() {
let out = compile_and_emit_with(
"SELECT Post { n := count((SELECT .<posts FILTER .age > 1)) }",
&make_schema(),
);
assert!(
!out.sql.contains("count((SELECT"),
"an aggregate over a scalar subquery:\n{}",
out.sql
);
assert!(
out.sql.contains(r#"(SELECT count("_s"."v") FROM unnest(ARRAY("#),
"{}",
out.sql
);
}
#[test]
fn any_over_a_link_type_check_tests_each_element() {
let out = compile_and_emit_with(
"SELECT Company { name } FILTER any(.<company.posts IS Post)",
&make_schema(),
);
assert!(
!out.sql.contains("bool_or(ARRAY"),
"an aggregate over outer rows:\n{}",
out.sql
);
assert!(
out.sql
.contains("FROM unnest(ARRAY(SELECT ('default::Post' = 'default::Post')"),
"{}",
out.sql
);
}
#[test]
fn a_link_type_check_reads_the_linked_object() {
let out = compile_and_emit_with("SELECT Person { name } FILTER .company IS Company", &make_schema());
assert!(!out.sql.contains("WHERE FALSE"), "{}", out.sql);
assert!(out.sql.contains("'default::Company'"), "{}", out.sql);
}
fn schema_with_a_subtype() -> SchemaDescriptor {
let mut schema = make_schema();
let mut admin = schema.types[0].clone();
admin.name = "Admin".into();
admin.table = "Admin".into();
admin.bases = vec!["default::Person".into()];
schema.types.push(admin);
schema
}
#[test]
fn a_type_with_subtypes_reads_their_rows_too() {
let out = compile_and_emit_with("SELECT Person { name }", &schema_with_a_subtype());
assert!(
out.sql.contains("'default::Admin'::text AS \"__type__\"") && out.sql.contains("FROM \"public\".\"Admin\""),
"expected the subtype's table in the read:\n{}",
out.sql
);
}
#[test]
fn a_path_from_a_type_with_subtypes_reads_their_rows_too() {
let out = compile_and_emit_with("SELECT Person.name", &schema_with_a_subtype());
assert!(out.sql.contains("FROM \"public\".\"Admin\""), "{}", out.sql);
}
#[test]
fn a_multilink_of_a_type_with_subtypes_reads_their_junctions_too() {
let out = compile_and_emit_with("SELECT Person { posts: { title } }", &schema_with_a_subtype());
assert!(
out.sql.contains(
"(SELECT \"source\", \"target\" FROM \"public\".\"Person.posts\" UNION ALL \
SELECT \"source\", \"target\" FROM \"public\".\"Admin.posts\")"
),
"expected the junctions unioned:\n{}",
out.sql
);
}
#[test]
fn is_a_supertype_matches_its_subtypes() {
let out = compile_and_emit_with(
"SELECT Person { name } FILTER Person IS Admin",
&schema_with_a_subtype(),
);
assert!(out.sql.contains("\"__type__\" = 'default::Admin'"), "{}", out.sql);
}
#[test]
fn a_write_to_a_type_with_subtypes_reaches_their_tables() {
let out = compile_and_emit_with("UPDATE Person SET { age := 1 }", &schema_with_a_subtype());
assert!(
out.sql.contains("UPDATE \"public\".\"Person\"") && out.sql.contains("UPDATE \"public\".\"Admin\""),
"expected both tables updated:\n{}",
out.sql
);
assert!(
!out.sql.contains("UPDATE (\n"),
"a write never targets the union:\n{}",
out.sql
);
}
#[test]
fn test_comparing_a_multilink_itself_to_an_object() {
let out = compile_and_emit_with(
"WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER any(.posts = p)",
&make_schema(),
);
assert!(
out.sql.contains("EXISTS((SELECT 1\nFROM \"public\".\"Person.posts\""),
"expected a junction EXISTS:\n{}",
out.sql
);
assert!(
out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
"expected the junction target compared to the bound object's id:\n{}",
out.sql
);
}
#[test]
fn test_comparing_a_multilink_itself_warns_without_any() {
let ast = parse::parse("WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER .posts = p").unwrap();
let ir = ir::compile(&ast, &make_schema()).unwrap();
assert!(
ir.warnings.iter().any(|w| w.contains("multi-link '.posts'")),
"expected the set-valued FILTER warning, got: {:?}",
ir.warnings
);
}
#[test]
fn test_detached_names_the_type_of_a_nested_select() {
let out = compile_and_emit_with(
"INSERT Person { name := $n, company := (SELECT detached Company FILTER .name = $c LIMIT 1) }",
&make_schema(),
);
assert!(
out.sql.contains("INSERT INTO \"public\".\"Person\""),
"expected the insert to compile:\n{}",
out.sql
);
assert!(
out.sql.contains("FROM \"public\".\"Company\""),
"expected the detached select to read the Company table:\n{}",
out.sql
);
}
#[test]
fn test_select_over_an_update_on_a_with_binding() {
let out = compile_and_emit_with(
"WITH p := (SELECT Person FILTER .id = $a LIMIT 1) SELECT (UPDATE p SET { name := $n }) { id }",
&make_schema(),
);
assert!(
out.sql.contains("UPDATE \"public\".\"Person\""),
"expected the update to resolve to the bound type's table:\n{}",
out.sql
);
}
#[test]
fn test_a_mutation_as_a_free_object_field() {
let out = compile_and_emit_with(
"SELECT { renamed := (UPDATE Person FILTER .id = $a SET { name := $n }), n := 1 }",
&make_schema(),
);
assert!(
out.sql.contains("UPDATE \"public\".\"Person\""),
"expected the mutation to become a data-modifying CTE:\n{}",
out.sql
);
assert!(
out.sql
.contains("(SELECT \"t1\".\"id\"\nFROM \"_nested_dml_0\" AS \"t1\") AS \"_f0\""),
"expected the field to carry the rows the mutation touched:\n{}",
out.sql
);
}
#[test]
fn test_a_sub_selects_filter_stays_on_its_own_subject_when_a_field_chain_projects_off_it() {
let out = compile_and_emit_with("SELECT (SELECT Person FILTER .name = $n).company.name", &make_schema());
assert!(
out.sql.contains("WHERE (\"t0\".\"name\" = $1)"),
"the inner filter belongs to the Person alias:\n{}",
out.sql
);
}
#[test]
fn test_reading_back_a_nested_insert_through_its_own_statement() {
let out = compile_and_emit_with(
"SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) { id, posts: { title } }",
&make_schema(),
);
assert!(out.sql.contains("__ml_add_0"), "got:\n{}", out.sql);
}
#[test]
fn test_reading_back_a_nested_insert_within_its_own_statement() {
let out = compile_and_emit_with(
"SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) \
{ id, posts: { title } }",
&make_schema(),
);
assert!(
out.sql.contains("FROM \"_dml__ml_add_0\" AS \"jt\""),
"the junction rows must come from the CTE that wrote them:\n{}",
out.sql
);
assert!(
out.sql.contains("\"_nested_dml_0\""),
"and the targets from the nested insert's own CTE:\n{}",
out.sql
);
}
#[test]
fn test_a_nested_single_link_insert_reads_back_from_its_cte() {
let out = compile_and_emit_with(
"SELECT (INSERT Person { name := $n, company := (INSERT Company { name := $c }) }) \
{ id, company: { name } }",
&make_schema(),
);
assert!(
out.sql.contains("FROM \"_nested_dml_0\""),
"the linked row must be read from the CTE that inserted it:\n{}",
out.sql
);
}
#[test]
fn test_a_free_object_field_holding_an_object_keeps_it_an_object() {
let out = compile_and_emit_with(
"WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }",
&make_schema(),
);
assert!(
out.sql.contains("'default::Company'::text"),
"the field must carry the object's own row:\n{}",
out.sql
);
}
#[test]
fn test_projecting_an_object_off_a_free_object() {
let out = compile_and_emit_with(
"WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }.co",
&make_schema(),
);
assert!(
!out.sql.contains("jsonb_build_object"),
"projecting a pointer must not go through jsonb:\n{}",
out.sql
);
assert!(
out.sql.contains("SELECT v AS result, v FROM"),
"the object row is already the result row:\n{}",
out.sql
);
}
#[test]
fn test_a_shape_on_a_single_valued_object_path() {
let out = compile_and_emit_with("SELECT Person { c := .company { name } }", &make_schema());
assert!(
out.sql.contains("'default::Company'::text"),
"the pointer must carry the object's own row:\n{}",
out.sql
);
assert!(
!out.sql.contains("ARRAY(SELECT"),
"a single-valued walk is one object, not an array of one:\n{}",
out.sql
);
}
#[test]
fn test_a_shapeless_single_link_still_reads_as_its_value() {
let out = compile_and_emit_with("SELECT Person { c := .company }", &make_schema());
assert!(
!out.sql.contains("'default::Company'::text"),
"a bare link reference must not grow an object row:\n{}",
out.sql
);
}
#[test]
fn test_a_tuple_element_holding_a_shaped_object() {
let out = compile_and_emit_with(
"WITH c := (SELECT Company LIMIT 1) SELECT (c { name }, 1)",
&make_schema(),
);
assert!(
out.sql.contains("'default::Company'::text"),
"the element must carry the object's own row:\n{}",
out.sql
);
}
#[test]
fn test_an_object_returning_function_as_a_link_value() {
let mut schema = make_schema();
let company = schema
.types
.iter_mut()
.find(|t| t.name == "Company")
.expect("make_schema declares Company");
company.properties.push(PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
schema.functions.push(crate::schema::FunctionDescriptor {
name: "current_company".into(),
module: "default".into(),
params: vec![],
return_pg_type: "default::Company".into(),
return_is_object: true,
return_is_set: false,
return_is_polymorphic: false,
volatility: "stable".into(),
body: "select Company limit 1".into(),
});
let out = compile_and_emit_with("INSERT Person { name := $n, company := current_company() }", &schema);
assert!(
out.sql.contains("FROM \"public\".\"current_company\"()"),
"the function must supply the foreign key:\n{}",
out.sql
);
}
fn make_schema_with_an_object_returning_default() -> SchemaDescriptor {
let mut schema = make_schema();
let company = schema
.types
.iter_mut()
.find(|t| t.name == "Company")
.expect("make_schema declares Company");
company.properties.push(PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
schema.functions.push(crate::schema::FunctionDescriptor {
name: "current_company".into(),
module: "default".into(),
params: vec![],
return_pg_type: "default::Company".into(),
return_is_object: true,
return_is_set: false,
return_is_polymorphic: false,
volatility: "stable".into(),
body: "select Company limit 1".into(),
});
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
let company_link = person.links.iter_mut().find(|l| l.name == "company").unwrap();
company_link.default_pyql = Some("current_company()".into());
schema
}
#[test]
fn an_object_returning_link_default_is_applied_by_the_insert() {
let schema = make_schema_with_an_object_returning_default();
let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
assert!(
out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
"the default must supply the foreign key:\n{}",
out.sql
);
}
#[test]
fn an_explicit_value_wins_over_an_inlined_default() {
let schema = make_schema_with_an_object_returning_default();
let out = compile_and_emit_with("INSERT Person { name := $n, company := {} }", &schema);
assert!(
!out.sql.contains("current_company"),
"the shape named the pointer, so the default does not apply:\n{}",
out.sql
);
}
#[test]
fn an_object_returning_link_default_gets_no_column_default() {
let schema = make_schema_with_an_object_returning_default();
let ddl = crate::export::export_schema(&schema).unwrap();
assert!(
!ddl.contains("\"company_id\" uuid DEFAULT"),
"the column must carry no default:\n{ddl}"
);
crate::validate::validate_schema_types(&schema).expect("an inlined default is not an error");
}
#[test]
fn a_column_expressible_default_stays_in_the_ddl() {
let mut schema = make_schema();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
age.default_pyql = Some("21".into());
assert!(
crate::ir::inlined_pointer_defaults(schema.types.iter().find(|t| t.name == "Person").unwrap(), &schema)
.is_empty()
);
let ddl = crate::export::export_schema(&schema).unwrap();
assert!(ddl.contains("DEFAULT 21"), "{ddl}");
let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
assert!(
!out.sql.contains("21"),
"the column DEFAULT still applies it:\n{}",
out.sql
);
}
fn make_schema_with_an_inherited_self_naming_computed() -> SchemaDescriptor {
let mut schema = make_schema();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
person.computed.push(crate::schema::ComputedDescriptor {
name: "own_name".into(),
expression: "default::Person.name".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
});
let mut employee = person.clone();
employee.name = "Employee".into();
employee.table = "Employee".into();
employee.bases = vec!["default::Person".into()];
schema.types.push(employee);
schema
}
#[test]
fn an_inherited_computed_naming_its_declaring_type_still_means_the_row() {
let schema = make_schema_with_an_inherited_self_naming_computed();
let out = compile_and_emit_with("SELECT Employee { own_name }", &schema);
assert!(
!out.sql.contains("\"Person\""),
"the computed must read the Employee row, not Person's table:\n{}",
out.sql
);
crate::validate::validate_schema_types(&schema).expect("an inherited computed is single-valued");
}
#[test]
fn an_inherited_type_check_computed_tests_the_row_it_is_read_on() {
let mut schema = make_schema();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
person.computed.push(crate::schema::ComputedDescriptor {
name: "employed".into(),
expression: "(default::Person is default::Employee)".into(),
return_type: Some("boolean".into()),
link_target: None,
link_multi: false,
});
let mut employee = person.clone();
employee.name = "Employee".into();
employee.table = "Employee".into();
employee.bases = vec!["default::Person".into()];
schema.types.push(employee);
crate::validate::validate_schema_types(&schema).expect("the check is single-valued on both types");
let out = compile_and_emit_with("SELECT Employee { employed }", &schema);
assert!(
!out.sql.contains("\"Person\""),
"the check must read the Employee row's own type:\n{}",
out.sql
);
}
#[test]
fn a_shape_writing_the_computed_out_itself_does_not_get_the_anchor() {
let schema = make_schema_with_an_inherited_self_naming_computed();
let ast = parse::parse("SELECT Employee { own_name := default::Person.name }").expect("parse failed");
let Err(error) = ir::compile(&ast, &schema) else {
panic!("an absolute path has nothing to anchor to here")
};
assert!(
format!("{error:?}").contains("absolute paths are not valid"),
"{error:?}"
);
}
#[test]
fn a_default_that_compiles_nowhere_is_still_an_error() {
let mut schema = make_schema();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
age.default_pyql = Some("sequence_next(INTROSPECT default::AgeSeq)".into());
let errs = crate::validate::validate_schema_types(&schema).unwrap_err();
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("default"), "{msg}");
}
#[test]
fn test_comparing_a_link_with_an_object_returning_function() {
let mut schema = make_schema();
let company = schema
.types
.iter_mut()
.find(|t| t.name == "Company")
.expect("make_schema declares Company");
company.properties.push(PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
schema.functions.push(crate::schema::FunctionDescriptor {
name: "current_company".into(),
module: "default".into(),
params: vec![],
return_pg_type: "default::Company".into(),
return_is_object: true,
return_is_set: false,
return_is_polymorphic: false,
volatility: "stable".into(),
body: "select Company limit 1".into(),
});
let out = compile_and_emit_with("SELECT Person FILTER .company = current_company()", &schema);
assert!(
out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
"the link's key against the function's row:\n{}",
out.sql
);
}
#[test]
fn test_choosing_between_two_object_sets() {
let out = compile_and_emit_with(
"WITH a := (SELECT Person FILTER .name = $n), b := (SELECT Person) \
SELECT (a IF EXISTS a ELSE b) { name }",
&make_schema(),
);
assert!(
out.sql.contains("UNION ALL"),
"expected one branch per side:\n{}",
out.sql
);
}
#[test]
fn test_a_union_operand_written_inline() {
let out = compile_and_emit_with(
"SELECT ((SELECT Person FILTER .name = $n) UNION (SELECT Person)) { name }",
&make_schema(),
);
assert!(
out.sql.starts_with("WITH"),
"expected the operands hoisted:\n{}",
out.sql
);
assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
}
#[test]
fn test_a_for_loop_variable_over_objects() {
let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p.name)", &make_schema());
assert!(
out.sql.contains("FROM \"public\".\"Person\"") && out.sql.contains("\"_for_p\".\"v\""),
"the body must read the row the variable names:\n{}",
out.sql
);
}
#[test]
fn test_a_shape_on_a_for_loop_variable() {
let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p { name })", &make_schema());
assert!(
out.sql.contains("'default::Person'::text"),
"a shape on the variable yields its object:\n{}",
out.sql
);
}
#[test]
fn test_a_for_loop_variable_over_values_is_still_a_scalar() {
let out = compile_and_emit_with("FOR n IN {1, 2} UNION (SELECT n)", &make_schema());
assert!(out.sql.contains("VALUES"), "{}", out.sql);
}
#[test]
fn test_exists_over_a_sub_select_on_a_relative_path() {
let out = compile_and_emit_with(
"SELECT Person { id } FILTER EXISTS ((SELECT .posts FILTER .title = $t))",
&make_schema(),
);
assert!(out.sql.contains("EXISTS("), "{}", out.sql);
assert!(
out.sql.contains("\"Person.posts\""),
"the junction join must survive into the EXISTS:\n{}",
out.sql
);
assert!(
out.sql.contains("\"t0\".\"id\""),
"and it must correlate to the enclosing row:\n{}",
out.sql
);
}
#[test]
fn test_counting_the_rows_a_mutation_touched() {
let out = compile_and_emit_with("SELECT count((DELETE Person FILTER .name = $n))", &make_schema());
assert!(out.sql.starts_with("WITH"), "{}", out.sql);
assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""), "{}", out.sql);
assert!(out.sql.contains("count(*)"), "{}", out.sql);
}
#[test]
fn test_a_conflict_target_naming_several_columns() {
let out = compile_and_emit_with(
"INSERT Person { name := $n } UNLESS CONFLICT ON (.name, .id) ELSE (UPDATE Person SET { name := $n })",
&make_schema(),
);
assert!(
out.sql.contains("ON CONFLICT (\"name\", \"id\")"),
"expected a column list:\n{}",
out.sql
);
}
#[test]
fn test_a_for_loop_over_a_path() {
let out = compile_and_emit_with(
"WITH p := (SELECT Person LIMIT 1) FOR post IN p.posts UNION (SELECT post.title)",
&make_schema(),
);
assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
}
#[test]
fn test_a_relative_sub_select_keeps_its_enclosing_object() {
let out = compile_and_emit_with(
"SELECT Person { ps := assert_exists((SELECT .posts { title })) }",
&make_schema(),
);
assert!(out.sql.contains("assert_exists"), "{}", out.sql);
assert!(
out.sql.contains("\"Person.posts\""),
"the walk must reach the junction:\n{}",
out.sql
);
}
#[test]
fn test_a_relative_sub_select_as_a_multi_link_value() {
let out = compile_and_emit_with(
"UPDATE Person FILTER .name = $n SET { posts := (SELECT .posts FILTER .title = $t) }",
&make_schema(),
);
assert!(
out.sql.contains("\"Person.posts\""),
"the value must read the row's own posts:\n{}",
out.sql
);
}
#[test]
fn test_a_shape_written_after_a_sub_select() {
let out = compile_and_emit_with(
"SELECT Person { ps := (SELECT Post FILTER .title = $t) { title } }",
&make_schema(),
);
assert!(
out.sql.contains("'default::Post'::text"),
"the pointer must carry the object rows:\n{}",
out.sql
);
}
#[test]
fn test_a_shape_after_a_sub_select_with_a_with_block() {
let out = compile_and_emit_with(
"SELECT Person { ps := (WITH t := $t SELECT Post FILTER .title = t) { title } }",
&make_schema(),
);
assert!(out.sql.contains("'default::Post'::text"), "{}", out.sql);
}
#[test]
fn test_a_computed_declared_as_an_object_returning_call() {
let mut schema = make_schema();
schema.functions.push(crate::schema::FunctionDescriptor {
name: "recent".into(),
module: "default".into(),
params: vec![],
return_pg_type: "default::Post".into(),
return_is_object: true,
return_is_set: true,
return_is_polymorphic: false,
volatility: "stable".into(),
body: "select Post".into(),
});
schema.types[0].computed.push(crate::schema::ComputedDescriptor {
name: "recent".into(),
expression: "default::recent()".into(),
return_type: None,
link_target: None,
link_multi: false,
});
let out = compile_and_emit_with("SELECT Person { recent }", &schema);
assert!(
out.sql.contains("ARRAY(SELECT") && out.sql.contains("'default::Post'::text"),
"the pointer must carry the function's rows:\n{}",
out.sql
);
}
#[test]
fn test_one_branch_of_an_object_if_else_is_empty() {
let out = compile_and_emit_with(
"WITH p := (SELECT Person LIMIT 1) SELECT p { name } IF EXISTS p ELSE {}",
&make_schema(),
);
assert!(!out.sql.contains("UNION ALL"), "nothing to union with:\n{}", out.sql);
assert!(out.sql.contains("'default::Person'::text"), "{}", out.sql);
}
#[test]
fn test_a_conditional_insert_is_guarded_by_its_condition() {
let out = compile_and_emit_with("SELECT (INSERT Person { name := $n }) IF FALSE ELSE {}", &make_schema());
assert!(
out.sql.contains("SELECT") && out.sql.contains("WHERE"),
"a guarded insert selects its row rather than listing values:\n{}",
out.sql
);
assert!(!out.sql.contains("VALUES"), "VALUES always yields a row:\n{}", out.sql);
}
#[test]
fn test_a_conditional_update_carries_its_condition() {
let out = compile_and_emit(
"SELECT (UPDATE Person FILTER .name = $n SET { name := $m }) IF EXISTS (SELECT Company) ELSE {}",
);
let update = out.sql.find("UPDATE").expect("an update is emitted");
let where_clause = out.sql[update..].find("WHERE").expect("the update is filtered");
assert!(
out.sql[update + where_clause..].contains("EXISTS"),
"the condition narrows the rows the update touches:\n{}",
out.sql
);
}
#[test]
fn test_a_conditional_delete_carries_its_condition() {
let out = compile_and_emit("SELECT (DELETE Person FILTER .name = $n) IF FALSE ELSE {}");
let delete = out
.sql
.split("DELETE FROM")
.nth(1)
.expect("the delete should still be emitted");
let where_clause = delete.split("WHERE").nth(1).expect("the delete should be filtered");
assert!(
where_clause.contains("FALSE") || where_clause.contains("false"),
"the condition must narrow the delete itself:\n{}",
out.sql
);
}
#[test]
fn test_a_pointer_a_binding_declared_in_its_own_shape() {
let out = compile_and_emit_with(
"WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT p { name, co: { name } }",
&make_schema(),
);
assert!(
out.sql.contains("'default::Company'::text"),
"the declared pointer must be inlined:\n{}",
out.sql
);
}
#[test]
fn test_a_binding_declared_pointer_read_from_a_tuple_element() {
let out = compile_and_emit_with(
"WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT (p { co: { name } }, 1)",
&make_schema(),
);
assert!(out.sql.contains("'default::Company'::text"), "{}", out.sql);
}
#[test]
fn test_a_for_loop_variable_shaped_inside_a_tuple() {
let out = compile_and_emit_with(
"WITH p := (SELECT Person LIMIT 1) FOR q IN p.posts UNION (SELECT (a := q { title }, b := 1))",
&make_schema(),
);
assert!(
out.sql.contains("\"_for_q\".\"v\""),
"the element must be narrowed to the loop's row:\n{}",
out.sql
);
}
#[test]
fn test_choosing_between_two_walks() {
let out = compile_and_emit_with(
"WITH p := (SELECT Person LIMIT 1), q := (SELECT p.posts LIMIT 1) IF TRUE ELSE (SELECT p.posts) \
SELECT q { title }",
&make_schema(),
);
assert!(out.sql.contains("UNION ALL"), "one branch per side:\n{}", out.sql);
}
#[test]
fn test_a_bare_coalesce_of_correlated_walks_reaching_many_objects() {
let out = compile_and_emit_with(
"SELECT Post { owners := .<posts[is Person] ?? .<posts[is Person] }",
&make_schema(),
);
assert!(
!out.sql.contains("COALESCE("),
"not a value-by-value choice:\n{}",
out.sql
);
assert!(
out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
"the arms are aggregated:\n{}",
out.sql
);
}
#[test]
fn test_a_union_of_correlated_walks_reaching_many() {
let out = compile_and_emit_with(
"SELECT Post { owners := (.<posts[is Person] UNION .<posts[is Person]) { name } }",
&make_schema(),
);
assert!(
out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
"the arms are aggregated:\n{}",
out.sql
);
}
#[test]
fn test_a_coalesce_of_correlated_walks_reaching_many() {
let out = compile_and_emit_with(
"SELECT Post { owners := (.<posts[is Person] ?? .<posts[is Person]) { name } }",
&make_schema(),
);
assert!(
out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
"the arms are aggregated:\n{}",
out.sql
);
}
#[test]
fn test_a_union_of_correlated_walks() {
let out = compile_and_emit_with(
"SELECT Post { owners := (SELECT (.<posts[is Person] UNION .<posts[is Person]) { name } LIMIT 1) }",
&make_schema(),
);
assert!(out.sql.contains("UNION ALL"), "one arm per operand:\n{}", out.sql);
assert!(
out.sql.contains("AS \"_u\""),
"the arms are read as one set:\n{}",
out.sql
);
}
#[test]
fn test_a_for_loop_whose_body_updates() {
let out = compile_and_emit_with(
"FOR p IN (SELECT Person) UNION (UPDATE Person FILTER .id = p.id SET { name := $n })",
&make_schema(),
);
assert!(out.sql.contains("UPDATE \"public\".\"Person\""), "{}", out.sql);
assert!(
out.sql.contains("FROM \"_for_p\""),
"driven from the iteration:\n{}",
out.sql
);
assert!(
!out.sql.contains("LATERAL"),
"DML cannot sit in a LATERAL:\n{}",
out.sql
);
}
#[test]
fn test_a_for_loop_body_appends_the_row_it_is_iterating() {
let out = compile_and_emit(
"WITH t := (SELECT Post) FOR q IN t UNION (UPDATE Person FILTER .id = $i SET { posts += q })",
);
assert!(
out.sql.contains("\"_iter\"") && out.sql.contains("FROM \"_ids\""),
"junction rows driven from the iteration:\n{}",
out.sql
);
assert!(
!out.sql.contains("LATERAL"),
"DML cannot sit in a LATERAL:\n{}",
out.sql
);
}
#[test]
fn test_select_on_a_for_loop_variable_reads_only_its_row() {
let out = compile_and_emit("FOR p IN (SELECT Person) UNION (SELECT (SELECT p) { name })");
assert!(
out.sql.contains("= \"_for_p\".\"v\""),
"narrowed to the row the variable holds:\n{}",
out.sql
);
}
#[test]
fn test_a_walk_off_a_for_loop_variable_reads_the_binding() {
let out = compile_and_emit("WITH ps := (SELECT Post) FOR q IN ps UNION (SELECT Person FILTER .name = q.title)");
assert!(
out.sql.contains("FROM \"ps\""),
"the walk reads the binding:\n{}",
out.sql
);
}
#[test]
fn test_a_mutating_for_loop_binding_becomes_sibling_ctes() {
let out = compile_and_emit(
"WITH made := (FOR n IN {'a', 'b'} UNION (INSERT Post { title := n })) SELECT count(made)",
);
assert!(
out.sql.contains("\"made__ids\" AS (\nINSERT INTO"),
"the insert is its own top-level CTE:\n{}",
out.sql
);
assert!(
!out.sql.contains("LATERAL"),
"DML cannot sit in a LATERAL:\n{}",
out.sql
);
}
#[test]
fn test_count_of_a_binding_counts_its_rows() {
let out = compile_and_emit("WITH people := (SELECT Person) SELECT count(people)");
assert!(out.sql.contains("(SELECT count(*) FROM \"people\")"), "{}", out.sql);
}
#[test]
fn test_dividing_integers_yields_a_float() {
for (query, expected) in [
("SELECT 100 / 133", "((100)::float8 / 133)"),
("SELECT Person { a := .age / 2 }", "::float8 / 2)"),
("SELECT Person.age / 2", "::float8 / 2)"),
(
"WITH total := max({133}), current := 38 - 1 SELECT 100 / total * current",
"(((100)::float8 / (SELECT \"v\" FROM \"total\")) * (SELECT \"v\" FROM \"current\"))",
),
(
"WITH p := (SELECT Person LIMIT 1), index := p.age - 1, total := max(Person.age) SELECT 100 / total * index",
"((100)::float8 / (SELECT \"v\" FROM \"total\"))",
),
] {
let out = compile_and_emit(query);
assert!(out.sql.contains(expected), "{query}:\n{}", out.sql);
}
}
#[test]
fn test_dividing_anything_but_two_integers_is_left_to_postgres() {
for query in [
"SELECT 100 // 133",
"SELECT 100 / 1.5",
"SELECT <decimal>100 / <decimal>3",
"SELECT 100 / <decimal>3",
] {
let out = compile_and_emit(query);
assert!(!out.sql.contains("::float8 /"), "{query}:\n{}", out.sql);
}
}
#[test]
fn test_a_free_object_under_a_condition_stays_an_object() {
for query in [
"SELECT { a := 1 } IF true ELSE {}",
"SELECT {} IF true ELSE { a := 1 }",
"SELECT { a := 1 } IF true ELSE {} LIMIT 1",
] {
let out = compile_and_emit(query);
assert!(
matches!(&out.shape.root, ShapeNode::Object { type_name: None, pointers, .. } if pointers.len() == 1),
"{query}: {:?}\n{}",
out.shape.root,
out.sql
);
}
}
#[test]
fn test_exists_of_a_binding_asks_for_any_row() {
let out = compile_and_emit("WITH people := (SELECT Person) SELECT exists people");
assert!(out.sql.contains("EXISTS(SELECT 1 FROM \"people\")"), "{}", out.sql);
let out = compile_and_emit("WITH names := {'a', 'b'} SELECT exists names");
assert!(
out.sql
.contains("EXISTS(SELECT 1 FROM \"names\" WHERE \"v\" IS NOT NULL)"),
"{}",
out.sql
);
}
#[test]
fn test_a_conditional_insert_beside_a_read_branch_unions_both() {
let out = compile_and_emit(
"WITH existing := (SELECT Person FILTER .name = $n LIMIT 1) \
SELECT (existing IF EXISTS existing ELSE (INSERT Person { name := $n })) { name }",
);
assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
assert!(
out.sql.contains("INSERT INTO") && out.sql.contains("WHERE (NOT EXISTS(SELECT 1 FROM \"existing\"))"),
"the insert carries the condition itself:\n{}",
out.sql
);
}
fn make_schema_with_a_plain_abstract() -> SchemaDescriptor {
let mut schema = make_schema();
let mut archivable = schema
.types
.iter()
.find(|t| t.name == "Person")
.expect("Person is in the test schema")
.clone();
archivable.name = "Archivable".into();
archivable.table = "Archivable".into();
archivable.abstract_ = true;
archivable.materialized = false;
archivable.links = vec![];
archivable.multilinks = vec![];
schema.types.push(archivable);
let person = schema
.types
.iter_mut()
.find(|t| t.name == "Person")
.expect("just found");
person.parents.push("default::Archivable".into());
schema
}
#[test]
fn test_an_intersection_with_a_plain_abstract_fans_out() {
let schema = make_schema_with_a_plain_abstract();
let out = compile_and_emit_with("SELECT Post { who := [is default::Archivable].name }", &schema);
assert!(
!out.sql.contains("\"Archivable\""),
"a plain abstract backs no relation:\n{}",
out.sql
);
assert!(
out.sql.contains("\"public\".\"Person\""),
"read from the type that carries the column:\n{}",
out.sql
);
}
fn make_schema_with_a_linking_plain_abstract() -> SchemaDescriptor {
let mut schema = make_schema_with_a_plain_abstract();
let person_links = schema
.types
.iter()
.find(|t| t.name == "Person")
.expect("Person is in the test schema")
.links
.clone();
let archivable = schema
.types
.iter_mut()
.find(|t| t.name == "Archivable")
.expect("just added");
archivable.links = person_links;
schema
}
#[test]
fn test_a_plain_abstract_in_a_subquery_fans_out() {
let schema = make_schema_with_a_plain_abstract();
let out = compile_and_emit_with(
"SELECT Person FILTER .id IN (SELECT default::Archivable FILTER .age > 1).id",
&schema,
);
assert!(
!out.sql.contains("\"Archivable\""),
"a plain abstract backs no relation:\n{}",
out.sql
);
}
#[test]
fn test_a_backlink_narrowed_to_a_plain_abstract_fans_out() {
let schema = make_schema_with_a_linking_plain_abstract();
let out = compile_and_emit_with(
"SELECT Company FILTER .<company[is default::Archivable].name = 'a'",
&schema,
);
assert!(
!out.sql.contains("\"Archivable\""),
"a plain abstract backs no relation:\n{}",
out.sql
);
}
fn make_interface_schema_with_a_plain_abstract() -> SchemaDescriptor {
let mut schema = make_interface_schema();
let mut archivable = schema
.types
.iter()
.find(|t| t.name == "Individual")
.expect("Individual is in the interface schema")
.clone();
archivable.name = "Archivable".into();
archivable.table = "Archivable".into();
archivable.abstract_ = true;
archivable.materialized = false;
archivable.interfaces = vec![];
archivable.links = vec![];
archivable.multilinks = vec![];
archivable.computed = vec![];
schema.types.push(archivable);
for t in schema.types.iter_mut() {
match t.name.as_str() {
"Account" => t.materialized = true,
"Individual" => t.parents.push("default::Archivable".into()),
_ => {}
}
}
schema
}
#[test]
fn test_a_splat_over_a_plain_abstract_fans_out() {
let schema = make_interface_schema_with_a_plain_abstract();
let out = compile_and_emit_with("SELECT Account { id, [is default::Archivable].* }", &schema);
assert!(
!out.sql.contains("\"Archivable\""),
"a plain abstract backs no relation:\n{}",
out.sql
);
assert!(
out.sql.contains("first_name"),
"the splat still reaches the mixin's own pointers:\n{}",
out.sql
);
}
#[test]
fn test_an_aggregate_over_a_relative_walk_runs_inside_the_subquery() {
let out = compile_and_emit("SELECT Person { latest := max(.posts.title) }");
assert!(out.sql.contains("(SELECT max("), "{}", out.sql);
assert!(!out.sql.contains("max(ARRAY("), "{}", out.sql);
}
fn make_schema_with_a_computed_backlink() -> SchemaDescriptor {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
let company = schema
.types
.iter_mut()
.find(|t| t.name == "Company")
.expect("Company is in the test schema");
company.computed = vec![ComputedDescriptor {
name: "staff".into(),
expression: ".<company[is default::Person]".into(),
return_type: None,
link_target: None,
link_multi: false,
}];
schema
}
#[test]
fn test_a_computed_backlink_reads_the_objects_it_lands_on() {
let schema = make_schema_with_a_computed_backlink();
let out = compile_and_emit_with("SELECT Company { staff: { name } }", &schema);
assert!(
out.sql.contains("\"public\".\"Person\""),
"the walk reaches Person:\n{}",
out.sql
);
}
#[test]
fn test_a_computed_backlinks_modifiers_belong_to_what_it_lands_on() {
let schema = make_schema_with_a_computed_backlink();
let out = compile_and_emit_with("SELECT Company.staff { name } FILTER .name = 'ada'", &schema);
assert!(out.sql.contains("'ada'"), "{}", out.sql);
assert!(
out.sql.contains("\"public\".\"Person\""),
"filtered on the Person the walk lands on:\n{}",
out.sql
);
}
#[test]
fn test_an_unreserved_keyword_names_a_free_shape_field() {
let out = compile_and_emit("SELECT { last := 2, first := 1, order := 3 }");
assert!(out.sql.contains("\"last\""), "{}", out.sql);
assert!(out.sql.contains("\"first\""), "{}", out.sql);
assert!(out.sql.contains("\"order\""), "{}", out.sql);
}
#[test]
fn test_a_shaped_sub_select_over_a_type_is_an_object() {
let out = compile_and_emit("SELECT Person { latest := (SELECT Post { title } LIMIT 1) }");
assert!(out.sql.contains("\"title\""), "{}", out.sql);
}
#[test]
fn test_a_walk_inside_a_path_selects_shape_finds_its_row() {
let out = compile_and_emit("SELECT Person.posts { mine := (SELECT Post FILTER .title = .title LIMIT 1) }");
assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
}
#[test]
fn test_a_walk_off_a_binding_in_a_free_select() {
let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := p.company.name }");
assert!(out.sql.contains("\"p\""), "{}", out.sql);
assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
}
#[test]
fn test_a_free_selects_filter_can_root_at_a_binding() {
let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := 1 } FILTER p.company.name = 'Acme'");
assert!(out.sql.contains("'Acme'"), "{}", out.sql);
}
#[test]
fn test_a_trailing_shape_on_a_field_access_select() {
let out = compile_and_emit("SELECT (SELECT Person).company { name }");
assert!(out.sql.contains("\"name\""), "{}", out.sql);
assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
}
#[test]
fn test_a_union_of_walks_names_each_branch() {
let out = compile_and_emit(
"WITH p := (SELECT Person FILTER .name = $a), q := (SELECT Person FILTER .name = $b), \
t := (SELECT p.posts UNION q.posts) SELECT t { title }",
);
assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
assert!(out.sql.contains("\"title\""), "{}", out.sql);
}
#[test]
fn test_sibling_for_loops_each_get_their_own_iterator() {
let out = compile_and_emit(
"WITH a := (SELECT Post), b := (SELECT Post), \
x := (FOR p IN a UNION (INSERT Person { name := p.title, age := 1 })), \
y := (FOR p IN b UNION (INSERT Person { name := p.title, age := 2 })) \
SELECT x",
);
assert!(out.sql.contains("\"_for_p\" AS"), "{}", out.sql);
assert!(out.sql.contains("\"_for_p__1\" AS"), "{}", out.sql);
}
#[test]
fn test_a_nested_for_carries_the_outer_loops_key() {
let out = compile_and_emit(
"WITH made := (FOR p IN (SELECT Person) UNION ( \
FOR q IN p.posts UNION (INSERT Company { name := q.title }) \
)) SELECT count(made)",
);
assert!(
out.sql.contains("\"_outer\""),
"the inner iterator carries the bond:\n{}",
out.sql
);
assert!(
out.sql.contains("JOIN \"_for_p\" ON"),
"the insert reads both loop variables:\n{}",
out.sql
);
assert!(
!out.sql.contains("LATERAL (\nINSERT"),
"DML cannot sit in a LATERAL:\n{}",
out.sql
);
}
#[test]
fn test_a_link_value_can_be_a_walk_off_a_binding() {
let mut schema = make_schema();
let company = schema
.types
.iter_mut()
.find(|t| t.name == "Company")
.expect("make_schema declares Company");
company.properties.push(PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
let out = compile_and_emit_with(
"WITH others := (SELECT Person) \
SELECT (INSERT Person { name := $n, age := 1, company := (SELECT others.company LIMIT 1) })",
&schema,
);
assert!(out.sql.contains("\"company_id\""), "{}", out.sql);
}
#[test]
fn test_a_shape_over_a_coalesce_of_walks() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person { x := (.published ?? .plain) { title } }", &schema);
assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
assert!(
out.sql.contains("NOT EXISTS"),
"the later branch only stands in when the first is empty:\n{}",
out.sql
);
}
#[test]
fn test_an_aggregate_over_a_walk_onto_objects_counts_its_rows() {
let out = compile_and_emit("WITH c := (SELECT Company) SELECT c { n := count(.<company[is default::Person]) }");
assert!(out.sql.contains("count("), "{}", out.sql);
assert!(out.sql.contains("\"public\".\"Person\""), "{}", out.sql);
}
#[test]
fn an_aggregate_over_a_computed_set_counts_inside_a_subquery() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person { n := count(.plain) }", &schema);
assert!(
!out.sql.contains("count((SELECT"),
"the aggregate belongs inside the walk's subquery, not around it:\n{}",
out.sql
);
assert!(
out.sql.contains("(SELECT count(") && out.sql.contains("\"public\".\"Post\""),
"it counts the rows the computed's own path lands on:\n{}",
out.sql
);
}
#[test]
fn an_aggregate_over_a_stored_multi_link_still_counts_its_junction_rows() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person { n := count(.posts) }", &schema);
assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
assert!(
!out.sql.contains("JOIN \"public\".\"Post\""),
"counting junction rows needs no join onto the targets:\n{}",
out.sql
);
}
#[test]
fn test_a_select_can_name_its_own_result() {
let out = compile_and_emit("SELECT oldest := max(Person.age)");
assert!(out.sql.contains("max("), "{}", out.sql);
let filtered = compile_and_emit("SELECT n := Person.age FILTER n > 18");
assert!(
filtered.sql.contains("18"),
"the alias resolves to the result:\n{}",
filtered.sql
);
}
#[test]
fn test_exists_sees_a_computed_pointer() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person FILTER EXISTS .published", &schema);
assert!(out.sql.contains("EXISTS"), "{}", out.sql);
assert!(
out.sql.contains("\"public\".\"Post\""),
"the computed's own path:\n{}",
out.sql
);
}
#[test]
fn test_walking_through_a_computed_field_access() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema_with_computed_links();
let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
person.computed.push(ComputedDescriptor {
name: "latest_title".into(),
expression: "(select .posts order by .title desc limit 1).title".into(),
return_type: None,
link_target: None,
link_multi: false,
});
let out = compile_and_emit_with("SELECT Person { x := .latest_title }", &schema);
assert!(out.sql.contains("\"title\""), "{}", out.sql);
assert!(
out.sql.contains("LIMIT"),
"the computed's own limit survives:\n{}",
out.sql
);
}
#[test]
fn test_a_computed_field_access_onto_an_object_takes_a_shape() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema_with_computed_links();
let post = schema.types.iter_mut().find(|t| t.name == "Post").expect("Post");
post.links.push(LinkDescriptor {
name: "author".into(),
target: "default::Person".into(),
nullable: true,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
});
let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
person.computed.retain(|c| c.name != "looper");
person.computed.push(ComputedDescriptor {
name: "latest_author".into(),
expression: "((select .posts order by .title desc limit 1)).author".into(),
return_type: None,
link_target: None,
link_multi: false,
});
let out = compile_and_emit_with("SELECT Person { latest_author: { name } }", &schema);
assert!(out.sql.contains("\"name\""), "{}", out.sql);
assert!(
out.sql.contains("\"title\" DESC"),
"the order stays on the posts:\n{}",
out.sql
);
assert!(
out.sql.contains("LIMIT"),
"the computed's own limit survives:\n{}",
out.sql
);
let splat = compile_and_emit_with("SELECT Person { * }", &schema);
assert!(
!splat.sql.contains("latest_author"),
"`*` leaves links out:\n{}",
splat.sql
);
}
#[test]
fn test_a_narrowed_pointer_can_carry_a_shape() {
let out = compile_and_emit("SELECT Company { x := 1, [is default::Person].posts: { title } }");
assert!(out.sql.contains("\"title\""), "{}", out.sql);
assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
}
#[test]
fn test_select_type_name_as_a_path_step() {
let out = compile_and_emit("SELECT Person.__type__");
assert!(out.sql.contains("ROW('default::Person')"), "{}", out.sql);
assert!(out.sql.contains("\"Person\" AS \"t0\""), "{}", out.sql);
}
#[test]
fn test_aggregate_over_a_with_bound_object_set() {
let out = compile_and_emit("WITH people := (SELECT Person) SELECT max(people.age)");
assert!(out.sql.contains("\"people\" AS ("));
assert!(out.sql.contains("max("));
assert!(!out.sql.contains("FROM \"default\".\"Person\" AS \"t1\""));
}
#[test]
fn test_select_union_of_two_object_bindings() {
let out = compile_and_emit("WITH a := (SELECT Person LIMIT 1), b := (SELECT Person) SELECT (a UNION b)");
assert!(out.sql.contains("SELECT * FROM \"a\""));
assert!(out.sql.contains("UNION ALL"));
assert!(out.sql.contains("SELECT * FROM \"b\""));
}
#[test]
fn test_select_union_of_different_object_types_is_rejected() {
let schema = make_schema();
let ast = parse::parse("WITH a := (SELECT Person), b := (SELECT Company) SELECT (a UNION b)").unwrap();
let Err(err) = ir::compile(&ast, &schema) else {
panic!("union of unrelated types must be rejected")
};
assert!(format!("{err}").contains("UNION"));
}
#[test]
fn test_for_union_as_a_function_body() {
let schema = make_schema();
let descriptor = FunctionDescriptor {
name: "recent".into(),
module: "default".into(),
params: vec![],
return_pg_type: "default::Person".into(),
return_is_object: true,
return_is_set: true,
return_is_polymorphic: false,
volatility: "stable".into(),
body: "for n in {1, 2} union (select Person filter .age = n)".into(),
};
let ir = ir::compile_fn_body(&descriptor, &schema).expect("function body must compile");
let body = emit_fn_body(&ir);
assert!(body.contains("VALUES"), "{body}");
assert!(body.contains("CROSS JOIN LATERAL"), "{body}");
assert!(body.contains("\"_body\".*"), "{body}");
}
#[test]
fn test_with_binding_in_a_computed_reads_the_enclosing_object() {
let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
assert!(out.sql.contains("\"name\""), "{}", out.sql);
}
#[test]
fn test_subject_path_update_names_its_own_alias() {
let mut schema = make_schema();
let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
company.properties.insert(
0,
PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("uuidv7()".into()),
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
},
);
let out = compile_and_emit_with("SELECT (UPDATE Person.company SET { name := 'x' }) { name }", &schema);
assert!(out.sql.contains("\"t1\".\"id\" = ANY("), "{}", out.sql);
}
#[test]
fn test_an_update_past_a_from_returns_only_its_own_columns() {
let out = compile_and_emit(
"SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
);
assert!(out.sql.contains("FROM \"_nested_dml_0\""), "{}", out.sql);
assert!(
out.sql.contains("RETURNING \"t0\".*"),
"the update names its own target:\n{}",
out.sql
);
}
#[test]
fn test_nested_insert_in_a_mutation_written_as_a_free_set() {
let out = compile_and_emit(
"SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
);
assert!(
out.sql.contains("INSERT INTO \"public\".\"Company\""),
"the hoisted insert is defined, not just referenced:\n{}",
out.sql
);
}
#[test]
fn test_nested_insert_as_a_link_value_in_an_update() {
let out = compile_and_emit(
"SELECT (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) { name }",
);
assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
assert!(
out.sql.contains("FROM \"_nested_dml_0\""),
"the update reads the hoisted CTE:\n{}",
out.sql
);
}
#[test]
fn test_nested_insert_as_a_link_value() {
let out = compile_and_emit(
"SELECT (INSERT Person { name := 'a', company := (INSERT Company { name := 'c' }) }) { name }",
);
assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
}
#[test]
fn test_nested_insert_in_a_one_element_set_is_that_insert() {
let out = compile_and_emit(
"SELECT (INSERT Person { name := 'a', company := { (INSERT Company { name := 'c' }) } }) { name }",
);
assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
}
#[test]
fn test_insert_with_no_assignments_uses_default_values() {
let out = compile_and_emit("SELECT (INSERT Person { name := 'a', company := (INSERT Company {}) }) { name }");
assert!(out.sql.contains("DEFAULT VALUES"), "{}", out.sql);
}
#[test]
fn test_backlink_narrowed_to_an_interface_its_implementor_declares() {
use crate::schema::{LinkDescriptor, TypeDescriptor};
let mut schema = make_interface_schema();
let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
schema.types.push(TypeDescriptor {
name: "Note".into(),
module: "default".into(),
table: "Note".into(),
abstract_: false,
materialized: true,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: individual.properties[..1].to_vec(),
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
});
schema
.types
.iter_mut()
.find(|t| t.name == "Individual")
.unwrap()
.links
.push(LinkDescriptor {
name: "note".into(),
target: "default::Note".into(),
nullable: true,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
through: None,
});
let out = compile_and_emit_with("SELECT Note FILTER EXISTS .<note[is Account]", &schema);
assert!(out.sql.contains("\"Individual\""), "{}", out.sql);
}
#[test]
fn test_subject_reads_the_row_a_constraint_checks() {
let schema = make_schema();
let sql = ir::compile_constraint_expr("__subject__.age > 18", "default::Person", &schema)
.expect("__subject__ names the row under check");
assert!(sql.contains("\"age\""), "{sql}");
}
#[test]
fn test_free_select_filter_gates_the_result_and_warns() {
let schema = make_schema();
let ast = parse::parse("SELECT count(Person) FILTER (Person.age > 18)").unwrap();
let ir = ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(
out.sql.contains("EXISTS("),
"the condition gates the count:\n{}",
out.sql
);
assert!(
ir.warnings.iter().any(|w| w.contains("FILTER clause")),
"a set-valued filter warns: {:?}",
ir.warnings
);
}
#[test]
fn test_free_sub_select_with_modifiers_reads_inline() {
let out = compile_and_emit(
"SELECT Person { n := (WITH mine := .name SELECT count(Post) FILTER (Post.title = mine)) }",
);
assert!(out.sql.contains("_scalar_sub"), "{}", out.sql);
assert!(!out.sql.contains("WITH"), "nothing is hoisted:\n{}", out.sql);
}
#[test]
fn test_correlated_with_binding_is_not_hoisted() {
let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
assert!(
!out.sql.contains("\"own\" AS ("),
"the binding is inlined:\n{}",
out.sql
);
assert!(out.sql.contains("\"name\""), "{}", out.sql);
}
#[test]
fn test_array_literal_binding_is_readable_and_typed() {
let out = compile_and_emit("WITH order := ['a', 'b'] SELECT std::find(order, 'b')");
assert!(out.sql.contains("array_position"), "its type is known:\n{}", out.sql);
assert!(
out.sql.contains("AS v"),
"the binding exposes a value column:\n{}",
out.sql
);
}
#[test]
fn test_any_over_a_single_boolean_is_that_boolean() {
let out = compile_and_emit("SELECT Person FILTER std::any(.age > 18)");
assert!(!out.sql.contains("bool_or"), "no aggregate in WHERE:\n{}", out.sql);
assert!(out.sql.contains("> 18"), "{}", out.sql);
}
#[test]
fn test_comparing_a_value_to_a_set_is_membership() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person FILTER .name = Person.posts.title", &schema);
assert!(out.sql.contains("= ANY("), "{}", out.sql);
}
#[test]
fn test_for_over_a_with_binding_iterates_every_row() {
let out = compile_and_emit(
"WITH names := (SELECT Person.name) FOR n IN names UNION (SELECT Person FILTER .name = n)",
);
assert!(out.sql.contains("FROM \"names\""), "{}", out.sql);
assert!(!out.sql.contains("VALUES"), "the whole set is iterated:\n{}", out.sql);
}
#[test]
fn test_for_body_bindings_stay_inside_the_body() {
let out = compile_and_emit(
"WITH names := (SELECT Person.name) \
FOR n IN names UNION (WITH matching := (SELECT Person FILTER .name = n) SELECT matching)",
);
let lateral = out.sql.find("CROSS JOIN LATERAL").expect("a lateral body");
let binding = out.sql.find("\"matching\" AS (").expect("the body's binding");
assert!(binding > lateral, "the binding sits inside the body:\n{}", out.sql);
}
#[test]
fn test_array_valued_binding_picks_the_array_overload() {
let out = compile_and_emit("WITH l := <array<std::str>>$0 SELECT std::find(l, 'a')");
assert!(out.sql.contains("array_position"), "{}", out.sql);
assert!(!out.sql.contains("strpos"), "{}", out.sql);
}
#[test]
fn test_aggregate_over_a_path_in_a_filter_is_a_subquery() {
let out = compile_and_emit("SELECT Person FILTER .age = std::max(Person.age)");
assert!(
out.sql.contains("(SELECT max("),
"an aggregate in WHERE needs its own query:\n{}",
out.sql
);
}
#[test]
fn test_distinct_over_a_single_value_is_that_value() {
let out = compile_and_emit("SELECT Person { n := (DISTINCT .name) }");
assert!(out.sql.contains("\"name\""), "{}", out.sql);
assert!(!out.sql.contains("DISTINCT \""), "{}", out.sql);
}
#[test]
fn test_narrowing_an_interface_joins_the_implementor_table() {
let mut schema = make_interface_schema();
let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
account.materialized = true;
let out = compile_and_emit_with("SELECT Account[is Individual].first_name", &schema);
assert!(
out.sql.contains("JOIN \"public\".\"Individual\""),
"the implementor's own table is joined:\n{}",
out.sql
);
assert!(out.sql.contains("\"first_name\""), "{}", out.sql);
}
#[test]
fn test_sub_select_shape_declares_names_its_filter_and_projection_read() {
let out = compile_and_emit("SELECT Person { n := (SELECT .posts { t := .title } FILTER .t = 'x' LIMIT 1).t }");
assert!(out.sql.contains("'x'"), "{}", out.sql);
assert!(out.sql.contains("\"title\""), "{}", out.sql);
}
#[test]
fn test_assert_over_an_object_set_returns_rows() {
let out = compile_and_emit("SELECT std::assert_distinct((SELECT Person))");
assert!(
out.sql.contains("\"assert_distinct\"(ARRAY(SELECT \"id\""),
"{}",
out.sql
);
assert!(out.sql.contains("= ANY("), "{}", out.sql);
assert!(out.sql.contains("\"Person\" AS \"t1\""), "{}", out.sql);
assert!(!out.sql.contains("unnest("), "{}", out.sql);
}
#[test]
fn test_assert_over_a_for_union_reads_its_rows() {
let out =
compile_and_emit("SELECT std::assert_distinct((FOR n IN {1, 2} UNION (SELECT Person FILTER .age = n)))");
assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
assert!(out.sql.contains("= ANY("), "{}", out.sql);
}
#[test]
fn test_declared_computed_with_binding_reads_its_own_object() {
use crate::schema::ComputedDescriptor;
let schema = make_schema();
let cd = ComputedDescriptor {
name: "shouted".into(),
expression: "(with own := .name select std::str_upper(own))".into(),
return_type: None,
link_target: None,
link_multi: false,
};
let ir = ir::compile_computed_in_type(&cd, "default::Person", &schema)
.expect("a computed may read the object it is declared on");
assert!(ir.is_some());
}
#[test]
fn test_sub_select_modifiers_scope_to_its_own_subject() {
let mut schema = make_schema();
let post = schema
.types
.iter_mut()
.find(|t| t.name == "Post")
.expect("Post is in the test schema");
post.links.push(LinkDescriptor {
name: "author".into(),
target: "default::Person".into(),
nullable: true,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
through: None,
});
let out = compile_and_emit_with(
"SELECT Person { a := (SELECT .posts FILTER .title = 'x' LIMIT 1).author.name }",
&schema,
);
assert!(out.sql.contains("'x'"), "{}", out.sql);
}
#[test]
fn test_free_select_set_literal() {
let schema = make_schema();
let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
let ir = ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert_eq!(out.sql.matches("UNION ALL").count(), 2);
assert!(out.sql.contains("1 AS v"));
assert!(out.sql.contains("2 AS v"));
assert!(out.sql.contains("3 AS v"));
assert!(out.sql.contains("ROW(v) AS result"));
assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
}
#[test]
fn test_free_select_free_object() {
let schema = make_schema();
let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
let ir = ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(out.sql.contains("'bar'"));
assert!(out.sql.contains("42"));
assert!(out.sql.contains("AS result"));
let crate::query::ShapeNode::Object {
pointers, type_name, ..
} = &out.shape.root
else {
panic!("expected Object shape")
};
assert!(type_name.is_none());
assert_eq!(pointers.len(), 2);
assert!(matches!(&pointers[0], crate::query::ShapeNode::Scalar { name, position: 0 } if name == "foo"));
assert!(matches!(&pointers[1], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "n"));
}
#[test]
fn test_free_select_object_with_enum_field_casts_to_text_and_tags_shape() {
let mut schema = make_schema();
schema.enums.push(crate::schema::EnumDescriptor {
name: "Gender".into(),
module: "default".into(),
members: vec!["Male".into(), "Female".into()],
});
let out = compile_and_emit_with("select { gender := default::Gender.Male }", &schema);
assert!(
out.sql.contains("'Male'::\"public\".\"Gender\""),
"expected the enum literal, got:\n{}",
out.sql
);
assert!(
out.sql.contains("ROW(\"_f0\"::text) AS result"),
"expected the ROW composite to cast the enum field to text, got:\n{}",
out.sql
);
let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected Object shape")
};
assert_eq!(pointers.len(), 1);
assert!(
matches!(
&pointers[0],
crate::query::ShapeNode::Enum { name, position: 0, enum_type }
if name == "gender" && enum_type == "public::Gender"
),
"expected Enum-tagged shape, got: {:?}",
pointers[0]
);
}
#[test]
fn test_free_select_bare_enum_literal_casts_to_text_inside_row() {
let mut schema = make_schema();
schema.enums.push(crate::schema::EnumDescriptor {
name: "Gender".into(),
module: "default".into(),
members: vec!["Male".into(), "Female".into()],
});
let out = compile_and_emit_with("select default::Gender.Male", &schema);
assert!(
out.sql.contains("ROW(v::text) AS result"),
"expected ROW(v::text), got:\n{}",
out.sql
);
assert!(
matches!(
&out.shape.root,
crate::query::ShapeNode::Enum { enum_type, .. } if enum_type == "public::Gender"
),
"expected Enum-tagged shape, got: {:?}",
out.shape.root
);
}
#[test]
fn test_for_update_defaults_to_blocking() {
let out = compile_and_emit("SELECT Person FOR UPDATE");
assert!(out.sql.trim_end().ends_with("FOR UPDATE"), "got:\n{}", out.sql);
}
#[test]
fn test_for_update_skip_locked() {
let out = compile_and_emit("SELECT Person FOR UPDATE SKIP LOCKED");
assert!(
out.sql.trim_end().ends_with("FOR UPDATE SKIP LOCKED"),
"got:\n{}",
out.sql
);
}
#[test]
fn test_for_update_nowait() {
let out = compile_and_emit("SELECT Person FOR UPDATE NOWAIT");
assert!(out.sql.trim_end().ends_with("FOR UPDATE NOWAIT"), "got:\n{}", out.sql);
}
#[test]
fn test_for_no_key_update_skip_locked() {
let out = compile_and_emit("SELECT Person FOR NO KEY UPDATE SKIP LOCKED");
assert!(
out.sql.trim_end().ends_with("FOR NO KEY UPDATE SKIP LOCKED"),
"got:\n{}",
out.sql
);
}
#[test]
fn test_for_share() {
let out = compile_and_emit("SELECT Person FOR SHARE");
assert!(out.sql.trim_end().ends_with("FOR SHARE"), "got:\n{}", out.sql);
}
#[test]
fn test_for_key_share_nowait() {
let out = compile_and_emit("SELECT Person FOR KEY SHARE NOWAIT");
assert!(
out.sql.trim_end().ends_with("FOR KEY SHARE NOWAIT"),
"got:\n{}",
out.sql
);
}
#[test]
fn test_for_update_comes_after_order_by_limit_offset_in_emitted_sql() {
let out = compile_and_emit("SELECT Person { name } ORDER BY .name OFFSET 1 LIMIT 5 FOR UPDATE SKIP LOCKED");
let order_pos = out.sql.find("ORDER BY").unwrap();
let offset_pos = out.sql.find("OFFSET").unwrap();
let limit_pos = out.sql.find("LIMIT").unwrap();
let for_pos = out.sql.find("FOR UPDATE").unwrap();
assert!(
order_pos < offset_pos && offset_pos < limit_pos && limit_pos < for_pos,
"got:\n{}",
out.sql
);
}
#[test]
fn test_for_update_combined_with_distinct_is_rejected() {
let ast = parse::parse("SELECT DISTINCT Person FOR UPDATE").expect("parse failed");
let err = ir::compile(&ast, &make_schema())
.err()
.expect("expected a compile error");
assert!(err.to_string().contains("DISTINCT"), "unexpected: {err}");
}
#[test]
fn test_for_update_combined_with_select_over_insert_is_rejected() {
let ast = parse::parse("SELECT (INSERT Person { name := 'Alice' }) { name } FOR UPDATE").expect("parse failed");
let err = ir::compile(&ast, &make_schema())
.err()
.expect("expected a compile error");
assert!(err.to_string().contains("INSERT"), "unexpected: {err}");
}
#[test]
fn test_for_update_on_an_interface_type_is_rejected() {
fn id_prop() -> PropertyDescriptor {
PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("uuidv7()".into()),
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
}
}
let schema = SchemaDescriptor {
types: vec![
TypeDescriptor {
name: "Account".into(),
module: "default".into(),
table: "Account".into(),
abstract_: true,
materialized: true,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop()],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Individual".into(),
module: "default".into(),
table: "Individual".into(),
abstract_: false,
materialized: true,
description: None,
parents: vec![],
interfaces: vec!["default::Account".into()],
bases: vec![],
properties: vec![id_prop()],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
};
let ast = parse::parse("SELECT Account FOR UPDATE").expect("parse failed");
let err = ir::compile(&ast, &schema).err().expect("expected a compile error");
assert!(err.to_string().contains("interface"), "unexpected: {err}");
}
#[test]
fn test_schema_select_distinct_emits_distinct_keyword() {
let out = compile_and_emit("SELECT DISTINCT Person { name }");
assert!(out.sql.contains("DISTINCT"), "expected DISTINCT in SQL:\n{}", out.sql);
}
#[test]
fn test_schema_select_detached_compiles_as_ordinary_select() {
let out = compile_and_emit("SELECT DETACHED Person { name }");
assert!(
out.sql.contains("\"name\""),
"expected name column in SQL:\n{}",
out.sql
);
}
#[test]
fn test_detached_subquery_correlates_against_the_outer_row() {
let out = compile_and_emit(
"SELECT Person { name } FILTER NOT EXISTS (\
SELECT DETACHED Person FILTER .name = Person.name AND .age > Person.age)",
);
assert!(
out.sql.contains("\"t1\".\"name\" = \"t0\".\"name\""),
"the inner row should be compared against the outer one:\n{}",
out.sql
);
assert!(
!out.sql.contains("\"t1\".\"name\" = \"t1\".\"name\""),
"self-comparison means the correlation was lost:\n{}",
out.sql
);
}
#[test]
fn test_an_assert_on_a_pointer_checks_its_rows() {
let out = compile_and_emit("SELECT Person { name, p := assert_exists(.posts { title }) }");
assert!(
out.sql
.contains(r#"cardinality("_pylon"."assert_exists"("_a"."v"::text[]))"#),
"the assert should read the aggregated rows:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("array_agg(ROW(").count(),
1,
"the pointer's subquery should be evaluated once, not once per use:\n{}",
out.sql
);
}
#[test]
fn test_an_assert_on_a_scalar_stays_an_ordinary_call() {
let out = compile_and_emit("SELECT Person { n := assert_exists(.name) }");
assert!(
!out.sql.contains(r#""_a"."v""#),
"a scalar assert should not be wrapped as a pointer check:\n{}",
out.sql
);
}
#[test]
fn test_a_backlink_tail_walks_past_the_first_step() {
let out = compile_and_emit("SELECT Post FILTER any(.<posts[is Person].posts.title = $t)");
assert!(
out.sql.contains(r#""public"."Person.posts""#),
"the tail should walk the multi-link's junction:\n{}",
out.sql
);
}
#[test]
fn test_an_intersection_narrows_the_rest_of_a_multilink_walk() {
let out = compile_and_emit("SELECT Person FILTER any(.posts[is Post].title = $t)");
assert!(
out.sql.contains(r#""public"."Post""#),
"the walk should continue from the narrowed type:\n{}",
out.sql
);
}
#[test]
fn test_a_union_of_bindings_emits_selects_not_bare_names() {
let out = compile_and_emit_with(
"WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
SELECT (INSERT Person { name := $n, posts := (a UNION b) })",
&make_schema(),
);
assert!(
out.sql.contains(r#"(SELECT "_s"."id" FROM "a" AS "_s")"#),
"each union operand should be a select:\n{}",
out.sql
);
}
#[test]
fn test_a_bare_narrowing_reads_as_a_type_check_and_as_an_object() {
for query in [
"SELECT Person FILTER EXISTS [is Person]",
"SELECT Person { p := [is Person] { name } }",
] {
let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query}: {e}"));
assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
}
}
#[test]
fn test_a_multilink_takes_a_set_wrapped_in_a_select() {
let out = compile_and_emit_with(
"WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
SELECT (INSERT Person { name := $n, posts := (SELECT { a, b }) })",
&make_schema(),
);
assert!(
out.sql.contains(r#""public"."Person.posts""#),
"both targets should reach the junction:\n{}",
out.sql
);
}
#[test]
fn test_an_aggregate_over_a_subselected_path() {
let out = compile_and_emit("SELECT array_agg((SELECT Person.name))");
assert!(
out.sql.contains("array_agg") && out.sql.contains(r#""public"."Person""#),
"the aggregate should wrap the walk's column:\n{}",
out.sql
);
}
#[test]
fn test_a_distinct_aggregate_argument_reaches_the_aggregate() {
for query in [
"SELECT array_agg(DISTINCT Person.name)",
"SELECT array_agg((SELECT DISTINCT Person.name))",
] {
let out = compile_and_emit(query);
assert!(
out.sql.contains("array_agg(DISTINCT "),
"{query} should deduplicate inside the aggregate:\n{}",
out.sql
);
}
}
#[test]
fn test_is_not_negates_the_type_check() {
let positive = compile_and_emit("SELECT Person FILTER Person IS Person");
let negative = compile_and_emit("SELECT Person FILTER Person IS NOT Person");
assert!(
negative.sql.contains("NOT") && !positive.sql.contains("NOT"),
"the negation should reach the SQL:\npositive:\n{}\nnegative:\n{}",
positive.sql,
negative.sql
);
}
#[test]
fn test_a_cast_takes_a_prefix_operator_as_its_operand() {
for query in ["SELECT <bool>EXISTS (SELECT Person LIMIT 1)", "SELECT <int64>-1"] {
let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query} should parse: {e}"));
assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
}
}
#[test]
fn test_a_bare_multilink_reads_as_a_set() {
let out =
compile_and_emit("WITH p := (SELECT DETACHED Post LIMIT 1) SELECT Person { name } FILTER p IN .posts");
assert!(
out.sql.contains(r#""public"."Person.posts""#),
"the membership test should read the link's junction:\n{}",
out.sql
);
}
#[test]
fn test_a_select_reads_its_own_shape_computed_in_its_clauses() {
let out = compile_and_emit("SELECT Person { n := .name } ORDER BY .n ASC");
assert!(
out.sql.contains("ORDER BY"),
"the order by should compile against the declared pointer:\n{}",
out.sql
);
}
#[test]
fn test_a_declared_pointer_can_be_walked_through() {
let out = compile_and_emit_with(
"WITH p := (SELECT Person { boss := .company } LIMIT 1) SELECT p { name, boss_name := .boss.name }",
&make_schema(),
);
assert!(
out.sql.contains(r#""public"."Company""#),
"the walk should reach the declared pointer's own target table:\n{}",
out.sql
);
}
#[test]
fn test_a_replaced_subject_shape_stays_in_scope() {
let out = compile_and_emit("SELECT Person { c := (SELECT .company { n := .name }) { n } }");
assert!(
out.sql.contains(r#"'default::Company'::text"#) && out.sql.contains(r#""t2"."name""#),
"`n` should resolve to the company's own column:\n{}",
out.sql
);
}
#[test]
fn test_a_sibling_shape_pointer_is_not_in_scope() {
let schema = make_schema();
let ast = parse::parse("SELECT Person { n := .name, copy := .n }").unwrap();
assert!(ir::compile(&ast, &schema).is_err());
}
#[test]
fn test_shape_over_a_with_binding_reads_the_binding() {
let out = compile_and_emit("WITH c := (SELECT Company LIMIT 1) SELECT Person { name, employer := c { name } }");
assert!(
out.sql.contains("\"c\""),
"the pointer should read the binding's CTE:\n{}",
out.sql
);
}
#[test]
fn test_shape_over_a_bare_type_name_is_still_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT Person { name, other := Company { name } }").unwrap();
assert!(ir::compile(&ast, &schema).is_err());
}
#[test]
fn test_nested_shape_reads_a_prefix_bound_by_the_outer_select() {
let out = compile_and_emit("SELECT Person { name, posts: { title, who := Person.name } }");
assert!(
out.sql.contains(r#""t0"."name""#),
"the nested computed should read the outer Person alias:\n{}",
out.sql
);
assert!(
!out.sql.contains(r#"FROM "public"."Person" "t2""#),
"the prefix should not open a second Person source:\n{}",
out.sql
);
}
#[test]
fn test_prefix_naming_a_type_out_of_scope_is_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT Person { name, who := Company.name }").unwrap();
assert!(ir::compile(&ast, &schema).is_err());
}
#[test]
fn test_free_select_tuple() {
let schema = make_schema();
let ast = parse::parse("SELECT (1, 2)").unwrap();
let ir = ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(out.sql.contains("1"));
assert!(out.sql.contains("2"));
assert!(out.sql.contains("AS result"));
assert!(matches!(out.shape.root, crate::query::ShapeNode::Tuple { .. }));
}
#[test]
fn test_free_select_scalar_literal() {
let schema = make_schema();
let ast = parse::parse("SELECT 'hello'").unwrap();
let ir = ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(out.sql.contains("SELECT 'hello' AS v"));
assert!(out.sql.contains("ROW(v) AS result"));
assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
}
#[test]
fn test_float_literal_casts_to_float8() {
let out = compile_and_emit("SELECT 1.0");
assert!(
out.sql.contains("(1.0::float8)"),
"expected explicit float8 cast, got:\n{}",
out.sql
);
}
#[test]
fn test_free_select_array_literal() {
let schema = make_schema();
let ast = parse::parse("SELECT [1, 2, 3]").unwrap();
let ir = ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(out.sql.contains("SELECT ARRAY[1, 2, 3] AS v"), "{}", out.sql);
assert!(out.sql.contains("SELECT v AS result, v FROM"), "{}", out.sql);
assert!(matches!(out.shape.root, crate::query::ShapeNode::RawScalar));
}
#[test]
fn test_select_scalars() {
let out = compile_and_emit("SELECT Person { name, age }");
assert!(out.sql.contains("'default::Person'::text"));
assert!(out.sql.contains("\"name\"::text"));
assert!(out.sql.contains("\"age\"::int8"));
assert!(out.sql.contains("FROM \"public\".\"Person\""));
assert!(out.sql.contains(") AS result"));
}
#[test]
fn test_select_filter_param() {
let out = compile_and_emit("SELECT Person { name } FILTER .name = $name");
assert!(out.sql.contains("WHERE"));
assert!(out.sql.contains("$1"));
}
#[test]
fn test_filter_in_set_literal_compiles_to_any_array() {
let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol'}");
assert!(out.sql.contains("= ANY(ARRAY['Carol'])"), "got:\n{}", out.sql);
}
#[test]
fn test_filter_in_multi_element_set_literal_compiles_to_any_array() {
let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol', 'Bob'}");
assert!(out.sql.contains("= ANY(ARRAY['Carol', 'Bob'])"), "got:\n{}", out.sql);
}
#[test]
fn test_filter_in_array_unpack_passes_the_array_straight_to_any() {
let out = compile_and_emit("SELECT Person { name } FILTER .name IN std::array_unpack(<array<str>>$names)");
assert!(
!out.sql.contains("ANY(unnest("),
"unnest inside ANY is invalid in WHERE:\n{}",
out.sql
);
assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
}
#[test]
fn test_filter_not_in_array_unpack_passes_the_array_straight_to_all() {
let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN std::array_unpack(<array<str>>$names)");
assert!(
!out.sql.contains("ALL(unnest("),
"unnest inside ALL is invalid in WHERE:\n{}",
out.sql
);
assert!(out.sql.contains("<> ALL("), "got:\n{}", out.sql);
}
#[test]
fn test_all_over_unpacked_array_aggregates_the_array_not_the_multilink() {
let out = compile_and_emit(
"SELECT Person { name } FILTER all(std::array_unpack(<array<str>>$titles) IN .posts.title)",
);
assert!(out.sql.contains("bool_and("), "got:\n{}", out.sql);
assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
}
#[test]
fn test_any_over_a_multilink_against_an_unpacked_array_still_folds_into_any() {
let out = compile_and_emit(
"SELECT Person { name } FILTER any(.posts.title IN std::array_unpack(<array<str>>$titles))",
);
assert!(!out.sql.contains("unnest("), "got:\n{}", out.sql);
assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
}
#[test]
fn test_any_over_unpacked_array_aggregates_the_array_not_the_multilink() {
let out = compile_and_emit(
"SELECT Person { name } FILTER any(std::array_unpack(<array<str>>$titles) IN .posts.title)",
);
assert!(out.sql.contains("bool_or("), "got:\n{}", out.sql);
assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
}
fn schema_with_array_property() -> SchemaDescriptor {
let mut schema = make_schema();
schema.types[0].properties.push(crate::schema::PropertyDescriptor {
name: "perms".into(),
pg_type: "text[]".into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
schema
}
#[test]
fn test_aggregate_over_an_unpacked_walk_unnests_in_a_row_source() {
let out = compile_and_emit_with(
"WITH teams := (SELECT Person { perms } FILTER .age > 1) SELECT std::array_agg(std::array_unpack(teams.perms))",
&schema_with_array_property(),
);
assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
assert!(
out.sql.contains(
r#"coalesce(array_agg("_s"."v"), '{}') FROM unnest(ARRAY(SELECT unnest("t1"."perms") FROM "teams" AS "t1"))"#
),
"got:\n{}",
out.sql
);
}
#[test]
fn test_aggregate_over_an_unpacked_type_rooted_path_unnests_in_a_row_source() {
let out = compile_and_emit_with(
"SELECT std::count(std::array_unpack(Person.perms))",
&schema_with_array_property(),
);
assert!(!out.sql.contains("count(unnest("), "got:\n{}", out.sql);
assert!(
out.sql
.contains(r#"FROM unnest(ARRAY(SELECT unnest("t0"."perms") FROM "public"."Person" AS "t0"))"#),
"got:\n{}",
out.sql
);
}
#[test]
fn test_aggregate_over_an_unpacked_parameter_unnests_the_array_itself() {
let out = compile_and_emit("SELECT std::array_agg(std::array_unpack(<array<str>>$names))");
assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
assert!(
out.sql.contains(r#"FROM unnest(($1)::text[]) AS "_s"("v")"#),
"got:\n{}",
out.sql
);
}
#[test]
fn test_aggregate_over_an_unpacked_property_unnests_that_row_s_array() {
let out = compile_and_emit_with(
"SELECT Person { n := std::array_agg(std::array_unpack(.perms)) }",
&schema_with_array_property(),
);
assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
assert!(
out.sql.contains(r#"FROM unnest("t0"."perms") AS "_s"("v")"#),
"got:\n{}",
out.sql
);
}
#[test]
fn test_array_agg_over_nothing_is_an_empty_array_not_null() {
for query in [
"SELECT std::array_agg(Person.name)",
"SELECT std::array_agg((SELECT Person).name)",
] {
let out = compile_and_emit(query);
assert!(
out.sql.contains("coalesce(") && out.sql.contains("'{}'"),
"{query} got:\n{}",
out.sql
);
}
}
#[test]
fn test_an_array_agg_binding_stays_an_array_through_its_coalesce() {
let out = compile_and_emit(
"WITH ids := std::array_agg(Person.id) SELECT Person { name } FILTER std::contains(ids, .id)",
);
assert!(
out.sql.contains("@> ARRAY["),
"expected the array overload, got:\n{}",
out.sql
);
assert!(
!out.sql.contains("strpos("),
"the string overload must not win, got:\n{}",
out.sql
);
}
#[test]
fn test_aggregate_over_a_plain_walk_keeps_its_row_source() {
let out = compile_and_emit("SELECT std::array_agg(Person.name)");
assert!(
out.sql.contains(r#"array_agg("t0"."name")"#) && out.sql.contains(r#"FROM "public"."Person""#),
"got:\n{}",
out.sql
);
}
#[test]
fn test_the_cte_order_check_catches_a_forward_reference() {
let bad = "WITH \"b\" AS (\n SELECT * FROM \"a\"\n),\n\"a\" AS (\n SELECT 1\n)\nSELECT 1";
assert!(
super::forward_cte_reference(bad).is_some(),
"expected a forward reference"
);
let good = "WITH \"a\" AS (\n SELECT 1\n),\n\"b\" AS (\n SELECT * FROM \"a\"\n)\nSELECT 1";
assert_eq!(super::forward_cte_reference(good), None, "ordered CTEs must pass");
}
#[test]
fn test_a_cast_unpacked_array_still_folds_into_any() {
let out = compile_and_emit("SELECT Person { name } FILTER .name IN <str>std::array_unpack(<array<str>>$names)");
assert!(
!out.sql.contains("unnest("),
"the unpack must fold into ANY, got:\n{}",
out.sql
);
assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
}
#[test]
fn test_array_unpack_outside_in_still_unnests() {
let out = compile_and_emit("SELECT std::array_unpack(<array<str>>$names)");
assert!(out.sql.contains("unnest("), "got:\n{}", out.sql);
}
#[test]
fn test_filter_not_in_set_literal_compiles_to_all_array() {
let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN {'Carol'}");
assert!(out.sql.contains("<> ALL(ARRAY['Carol'])"), "got:\n{}", out.sql);
}
#[test]
fn test_deep_splat_fetches_single_link_target_properties_not_just_id() {
let out = compile_and_emit("SELECT Person { ** }");
assert!(
out.sql.contains("\"name\""),
"expected Company.name pulled in via .company's ** expansion, got:\n{}",
out.sql
);
}
#[test]
fn test_deep_splat_fetches_multilink_target_properties_not_just_id() {
let out = compile_and_emit("SELECT Person { ** }");
assert!(
out.sql.contains("\"title\""),
"expected Post.title pulled in via .posts' ** expansion, got:\n{}",
out.sql
);
}
#[test]
fn test_schema_type_cast_select() {
let out = compile_and_emit("SELECT <default::Person><uuid>'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'");
assert!(out.sql.contains("FROM \"public\".\"Person\""));
assert!(out.sql.contains("WHERE"));
assert!(out.sql.contains("'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'"));
}
#[test]
fn test_select_single_link() {
let out = compile_and_emit("SELECT Person { name, company { name } }");
assert!(out.sql.contains("'default::Company'::text"));
assert!(out.sql.contains("FROM \"public\".\"Company\""));
assert!(out.sql.contains("\"company_id\" = "));
}
#[test]
fn test_select_multi_link() {
let out = compile_and_emit("SELECT Person { name, posts { title } }");
assert!(out.sql.contains("array_agg(ROW("));
assert!(out.sql.contains("ARRAY[]::record[]"));
assert!(out.sql.contains("'default::Post'::text"));
assert!(out.sql.contains("\"Person.posts\""));
}
fn compile_err(query: &str) -> String {
let schema = make_schema();
let ast = parse::parse(query).expect("parse failed");
match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
}
}
#[test]
fn test_multi_link_limit_cuts_rows_before_aggregating() {
let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' limit 1 }");
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
let agg = out.sql.find("array_agg").unwrap();
let limit = out.sql.find("LIMIT 1").unwrap();
assert!(agg < limit, "{}", out.sql);
assert!(out.sql.contains("\"t1\".\"title\"::text AS \"c1\""), "{}", out.sql);
}
#[test]
fn test_multi_link_order_by_with_limit_orders_the_aggregate_too() {
let out = compile_and_emit("SELECT Person { posts: { title } order by .title desc offset 1 limit 2 }");
assert!(out.sql.contains("\"t1\".\"title\" AS \"s0\""), "{}", out.sql);
assert!(
out.sql.contains("ORDER BY \"__lim\".\"s0\" DESC NULLS LAST"),
"{}",
out.sql
);
assert!(out.sql.contains("OFFSET 1"), "{}", out.sql);
assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
}
#[test]
fn test_multi_link_without_limit_keeps_the_flat_aggregate() {
let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' }");
assert!(!out.sql.contains("__lim"), "{}", out.sql);
assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
}
#[test]
fn test_sub_select_as_computed_pointer_is_a_link_pointer() {
let out = compile_and_emit("SELECT Person { recent := (select .posts filter .title = 'x' limit 1) { title } }");
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
let ShapeNode::Object {
name,
pointers: elem_pointers,
..
} = &pointers[2]
else {
panic!("{:?}", pointers[2])
};
assert_eq!(name, "recent");
assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
}
#[test]
fn test_sub_select_shape_inside_the_parens_is_equivalent() {
let outer = compile_and_emit("SELECT Person { recent := (select .posts limit 1) { title } }");
let inner = compile_and_emit("SELECT Person { recent := (select .posts { title } limit 1) }");
assert_eq!(outer.sql, inner.sql);
}
#[test]
fn test_sub_select_field_access_is_a_correlated_scalar_subquery() {
let out = compile_and_emit("SELECT Person { t := (select .posts filter .title = 'x' limit 1).title }");
assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
assert!(matches!(&pointers[2], ShapeNode::Scalar { name, .. } if name == "t"));
}
#[test]
fn test_sub_select_field_access_over_a_single_link() {
let out = compile_and_emit("SELECT Person { c := (select .company).name }");
assert!(out.sql.contains("\"t1\".\"company_id\" = \"t2\".\"id\""), "{}", out.sql);
assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
}
#[test]
fn test_sub_select_field_access_in_a_filter() {
let out = compile_and_emit("SELECT Person { name } filter (select .posts limit 1).title = 'x'");
assert!(out.sql.contains("WHERE ((SELECT \"t2\".\"title\""), "{}", out.sql);
assert!(out.sql.contains("LIMIT 1) = 'x')"), "{}", out.sql);
}
#[test]
fn test_sub_select_over_an_absolute_path_is_not_correlated() {
let out = compile_and_emit("SELECT Person { n := (select Company.name limit 1) }");
assert!(out.sql.contains("FROM \"public\".\"Company\""), "{}", out.sql);
assert!(!out.sql.contains("= \"t0\".\"id\""), "{}", out.sql);
}
#[test]
fn test_backlink_sub_select_carries_its_modifiers() {
let out = compile_and_emit("SELECT Post { authors := (select .<posts[is Person] { name } limit 2) }");
assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
}
#[test]
fn test_with_bound_scalar_is_typed_for_overload_resolution() {
let out = compile_and_emit("WITH xs := (select Person.name) SELECT contains(xs, 'a')");
assert!(
out.sql.contains("strpos((SELECT \"v\" FROM \"xs\"), 'a')"),
"{}",
out.sql
);
}
fn make_schema_with_computed_links() -> SchemaDescriptor {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
person.computed = vec![
ComputedDescriptor {
name: "published".into(),
expression: "(select .posts filter .title != '')".into(),
return_type: None,
link_target: None,
link_multi: false,
},
ComputedDescriptor {
name: "plain".into(),
expression: ".posts".into(),
return_type: None,
link_target: None,
link_multi: false,
},
ComputedDescriptor {
name: "featured".into(),
expression: "(select .published filter .title != 'draft')".into(),
return_type: None,
link_target: None,
link_multi: false,
},
ComputedDescriptor {
name: "looper".into(),
expression: "(select .looper)".into(),
return_type: None,
link_target: None,
link_multi: false,
},
ComputedDescriptor {
name: "capped".into(),
expression: "(select .posts limit 1)".into(),
return_type: None,
link_target: None,
link_multi: false,
},
];
schema
}
fn make_schema_with_object_fn_computed() -> SchemaDescriptor {
use crate::schema::{ComputedDescriptor, FunctionDescriptor, FunctionParamDescriptor};
let mut schema = make_schema();
schema.functions = vec![FunctionDescriptor {
name: "latest".into(),
module: "default".into(),
params: vec![FunctionParamDescriptor {
name: "owner".into(),
pg_type: "uuid".into(),
}],
return_pg_type: "default::Post".into(),
return_is_object: true,
return_is_set: true,
return_is_polymorphic: false,
volatility: "stable".into(),
body: String::new(),
}];
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
person.computed = vec![
ComputedDescriptor {
name: "recent".into(),
expression: "latest(.id)".into(),
return_type: None,
link_target: None,
link_multi: false,
},
ComputedDescriptor {
name: "vetted".into(),
expression: "(select latest(.id) filter .title != '')".into(),
return_type: None,
link_target: None,
link_multi: false,
},
];
schema
}
#[test]
fn test_path_traverses_through_a_function_backed_computed() {
let schema = make_schema_with_object_fn_computed();
let out = compile_and_emit_with("SELECT Person { t := .recent.title }", &schema);
assert!(
out.sql
.contains("CROSS JOIN LATERAL \"public\".\"latest\"(\"t1\".\"id\") AS \"t2\""),
"{}",
out.sql
);
assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
}
#[test]
fn test_sub_select_over_a_function_backed_computed() {
let schema = make_schema_with_object_fn_computed();
let out = compile_and_emit_with(
"SELECT Person { t := (select .recent filter .title = 'x' limit 1).title }",
&schema,
);
assert!(
out.sql.contains("CROSS JOIN LATERAL \"public\".\"latest\""),
"{}",
out.sql
);
assert!(out.sql.contains("\"t2\".\"title\" = 'x'"), "{}", out.sql);
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
}
#[test]
fn test_function_backed_computed_carries_its_own_filter() {
let schema = make_schema_with_object_fn_computed();
let out = compile_and_emit_with(
"SELECT Person { t := (select .vetted filter .title = 'x' limit 1).title }",
&schema,
);
assert!(
out.sql
.contains("(\"t2\".\"title\" = 'x') AND (\"t2\".\"title\" <> '')"),
"{}",
out.sql
);
}
#[test]
fn test_function_backed_computed_checks_its_argument_count() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema_with_object_fn_computed();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
person.computed = vec![ComputedDescriptor {
name: "recent".into(),
expression: "latest()".into(),
return_type: None,
link_target: None,
link_multi: false,
}];
let ast = parse::parse("SELECT Person { t := .recent.title }").unwrap();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
};
assert!(err.contains("expects 1 argument(s), got 0"), "{err}");
}
#[test]
fn test_path_traverses_through_a_computed_pointer() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person { t := .published.title }", &schema);
assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
assert!(
out.sql
.contains("(\"t1\".\"id\" = \"t0\".\"id\") AND (\"t2\".\"title\" <> '')"),
"{}",
out.sql
);
}
#[test]
fn test_sub_select_over_a_computed_pointer_projects_a_property() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with(
"SELECT Person { t := (select .published filter .title = 'a' limit 1).title }",
&schema,
);
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
assert!(out.sql.contains("\"t2\".\"title\" = 'a'"), "{}", out.sql);
assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
}
#[test]
fn test_chained_computed_pointers_each_contribute_their_filter() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person { t := .featured.title }", &schema);
assert_eq!(out.sql.matches("\"public\".\"Person.posts\"").count(), 1, "{}", out.sql);
assert!(out.sql.contains("\"t2\".\"title\" <> 'draft'"), "{}", out.sql);
assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
}
#[test]
fn test_computed_pointer_naming_a_bare_link_is_traversable() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person { t := .plain.title }", &schema);
assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
}
#[test]
fn test_a_cycle_of_computed_pointers_is_reported_not_hung_on() {
let schema = make_schema_with_computed_links();
let ast = parse::parse("SELECT Person { t := .looper.title }").unwrap();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
};
assert!(err.contains("expands into itself"), "{err}");
}
#[test]
fn test_a_cycle_of_computed_pointers_read_as_a_value_is_reported_not_hung_on() {
let schema = make_schema_with_computed_links();
for query in ["SELECT Person { looper }", "SELECT Person { * }"] {
let ast = parse::parse(query).unwrap();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error for {query}"),
Err(e) => format!("{e}"),
};
assert!(err.contains("expands into itself"), "{query}: {err}");
}
}
#[test]
fn test_traversing_through_a_limited_computed_uses_a_lateral() {
let schema = make_schema_with_computed_links();
let out = compile_and_emit_with("SELECT Person { t := .capped.title }", &schema);
assert!(out.sql.contains("JOIN LATERAL ("), "{}", out.sql);
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
assert!(!out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
}
#[test]
fn test_backlink_from_a_type_whose_interface_the_link_targets() {
use crate::schema::{LinkDescriptor, TypeDescriptor};
let mut schema = make_interface_schema();
let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
schema.types.push(TypeDescriptor {
name: "Note".into(),
module: "default".into(),
table: "Note".into(),
abstract_: false,
materialized: true,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: individual.properties[..1].to_vec(),
links: vec![LinkDescriptor {
name: "owner".into(),
target: "default::Account".into(),
nullable: true,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
}],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
});
let out = compile_and_emit_with("SELECT Individual { notes := .<owner[is Note] }", &schema);
assert!(out.sql.contains("\"public\".\"Note\""), "{}", out.sql);
assert!(out.sql.contains("\"owner_id\" = \"t0\".id"), "{}", out.sql);
}
#[test]
fn test_with_block_in_expression_position_hoists_its_bindings() {
let out = compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select c) }");
assert!(out.sql.starts_with("WITH\n\"c\" AS ("), "{}", out.sql);
assert!(out.sql.contains("(SELECT \"v\" FROM \"c\")"), "{}", out.sql);
}
#[test]
fn test_hoisted_binding_is_usable_in_the_expression_around_it() {
let out =
compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select .name ++ c) }");
assert!(
out.sql.contains("(\"t0\".\"name\" || (SELECT \"v\" FROM \"c\"))"),
"{}",
out.sql
);
}
#[test]
fn test_hoisted_binding_joins_the_statements_own_with_clause() {
let out = compile_and_emit(
"WITH top := (select Person.name limit 1) \
SELECT Person { t := (with c := (select Company.name limit 1) select c) }",
);
let top = out.sql.find("\"top\" AS (").expect("top-level binding");
let inner = out.sql.find("\"c\" AS (").expect("hoisted binding");
assert!(top < inner, "the statement's own bindings come first:\n{}", out.sql);
}
#[test]
fn test_a_binding_named_after_a_keyword_is_usable() {
let out = compile_and_emit("WITH order := (select Person.name) SELECT Person { name } FILTER .name = order");
assert!(out.sql.contains("(SELECT \"v\" FROM \"order\")"), "{}", out.sql);
}
#[test]
fn test_computed_declared_on_an_interface_is_visible_from_an_implementor() {
use crate::schema::ComputedDescriptor;
let mut schema = make_interface_schema();
let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
account.computed = vec![ComputedDescriptor {
name: "tier".into(),
expression: "'gold'".into(),
return_type: None,
link_target: None,
link_multi: false,
}];
let individual = schema.types.iter_mut().find(|t| t.name == "Individual").unwrap();
individual.computed = vec![];
for q in ["SELECT Individual { tier }", "SELECT Individual { t := .tier }"] {
let out = compile_and_emit_with(q, &schema);
assert!(out.sql.contains("'gold'"), "{q}: {}", out.sql);
}
}
#[test]
fn test_computed_may_carry_trailing_modifiers_without_a_select() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
person.computed = vec![ComputedDescriptor {
name: "recent".into(),
expression: ".posts order by .title desc limit 5".into(),
return_type: None,
link_target: None,
link_multi: false,
}];
let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
assert!(out.sql.contains("ORDER BY \"t1\".\"title\" DESC"), "{}", out.sql);
}
#[test]
fn test_computed_may_lead_with_a_bare_select() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
person.computed = vec![ComputedDescriptor {
name: "recent".into(),
expression: "select .posts order by .title desc limit 5".into(),
return_type: None,
link_target: None,
link_multi: false,
}];
let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
}
#[test]
fn test_sub_select_with_a_shape_may_project_a_property_off_it() {
let out = compile_and_emit("SELECT Person { t := (select .posts { title } limit 1).title }");
assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
}
#[test]
fn test_sub_statement_error_names_what_it_got() {
let err = compile_err("SELECT Person { t := (insert Company { name := 'a' }) }");
assert!(err.contains("an insert cannot stand in for a value"), "{err}");
}
#[test]
fn test_with_bound_scalar_path_select_emits_a_real_select() {
let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name }");
assert!(
out.sql
.contains("SELECT ROW(v) AS result, v FROM (SELECT \"t0\".\"name\" AS v"),
"{}",
out.sql
);
}
#[test]
fn test_with_bound_scalar_path_select_is_typed_as_its_result() {
let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name } FILTER .name IN xs");
assert!(out.sql.contains("ANY((SELECT \"v\" FROM \"xs\"))"), "{}", out.sql);
}
#[test]
fn test_distinct_survives_a_with_binding() {
let out = compile_and_emit("WITH xs := (select distinct Person.name) SELECT Person { name }");
assert!(out.sql.contains("SELECT DISTINCT \"t0\".\"name\" AS v"), "{}", out.sql);
}
#[test]
fn test_for_over_a_derived_set_iterates_every_row() {
let out = compile_and_emit("FOR x IN (select Person.name) UNION (SELECT Person { name } FILTER .name = x)");
assert!(!out.sql.contains("VALUES"), "{}", out.sql);
assert!(out.sql.contains(") AS \"_for_x\""), "{}", out.sql);
assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
assert!(out.sql.contains("\"t1\".\"name\" = \"_for_x\".\"v\""), "{}", out.sql);
}
#[test]
fn test_for_insert_over_a_derived_set_uses_it_as_the_cte() {
let out = compile_and_emit("FOR x IN (select Person.name) UNION (INSERT Company { name := x })");
assert!(out.sql.starts_with("WITH \"_for_x\" AS ("), "{}", out.sql);
assert!(
out.sql.contains("SELECT \"_for_x\".\"v\" FROM \"_for_x\""),
"{}",
out.sql
);
}
#[test]
fn test_for_over_objects_binds_their_id() {
let out = compile_and_emit("FOR p IN (select Person) UNION (INSERT Company { name := <str>p })");
assert!(out.sql.contains("SELECT \"id\" AS v FROM ("), "{}", out.sql);
}
#[test]
fn test_for_over_a_non_select_statement_is_rejected() {
let ast = parse::parse("FOR x IN (INSERT Company { name := 'a' }) UNION (SELECT Person { name })").unwrap();
let schema = make_schema();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
};
assert!(err.contains("only a select can be iterated"), "{err}");
}
#[test]
fn test_multi_link_path_outside_a_comparison_is_an_array() {
let out = compile_and_emit("SELECT Person { t := .posts.title }");
assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
}
#[test]
fn test_multi_link_path_inside_a_comparison_is_still_exists() {
let out = compile_and_emit("SELECT Person { name } filter .posts.title = 'x'");
assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
}
#[test]
fn test_path_traversal_deeper_than_two_steps() {
let schema = make_schema_with_through();
let out = compile_and_emit_with("SELECT Person { t := .friends.friends.name }", &schema);
assert_eq!(out.sql.matches("\"public\".\"PersonFriend\"").count(), 2, "{}", out.sql);
assert!(out.sql.contains("ARRAY(SELECT \"t4\".\"name\""), "{}", out.sql);
}
#[test]
fn test_coalesce_over_backlinked_values_is_a_set_choice() {
let out = compile_and_emit("SELECT Post ORDER BY max(.<posts[is Person].age ?? .<posts[is Person].age)");
assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
assert!(!out.sql.contains("COALESCE("), "{}", out.sql);
assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
assert!(
out.sql
.contains("(SELECT coalesce(max(\"_s\".\"v\"), NULL) FROM unnest("),
"{}",
out.sql
);
}
#[test]
fn test_coalesce_over_multilink_values_is_a_set_choice() {
let out = compile_and_emit("SELECT Person { t := .posts.title ?? .posts.title }");
assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
}
#[test]
fn test_coalesce_of_a_set_with_a_single_value_falls_back_to_it() {
let out = compile_and_emit("SELECT Post { a := .<posts[is Person].age ?? 0 }");
assert!(out.sql.contains("ELSE array_remove(ARRAY[0], NULL) END"), "{}", out.sql);
}
#[test]
fn test_traversal_after_a_backlink() {
let out = compile_and_emit("SELECT Post { t := .<posts[is Person].name }");
assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"name\""), "{}", out.sql);
assert!(out.sql.contains("\"t3\".\"target\" = \"t1\".\"id\""), "{}", out.sql);
}
#[test]
fn test_computed_pointer_on_a_linked_type_is_reachable() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
company.computed = vec![ComputedDescriptor {
name: "shout".into(),
expression: ".name ++ '!'".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
}];
let out = compile_and_emit_with("SELECT Person { t := .company.shout }", &schema);
assert!(out.sql.contains("(\"t2\".\"name\" || '!')"), "{}", out.sql);
}
#[test]
fn test_computed_pointer_behind_a_type_intersection() {
let schema = make_interface_schema();
let out = compile_and_emit_with("SELECT Account { n := [is Individual].full_name }", &schema);
assert!(out.sql.contains("upper(\"t1\".\"first_name\")"), "{}", out.sql);
assert!(out.sql.contains("FROM \"public\".\"Individual\""), "{}", out.sql);
assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
}
#[test]
fn test_computed_pointer_cannot_be_traversed_through() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
company.computed = vec![ComputedDescriptor {
name: "shout".into(),
expression: ".name ++ '!'".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
}];
let ast = parse::parse("SELECT Person { t := .company.shout.nope }").unwrap();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
};
assert!(err.contains("is a computed pointer"), "{err}");
}
#[test]
fn test_sub_select_over_a_multi_link_path_keeps_its_modifiers() {
let out = compile_and_emit("SELECT Person { t := (select .posts.title order by .title desc limit 2) }");
assert!(out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
assert!(out.sql.contains("ORDER BY \"t2\".\"title\" DESC"), "{}", out.sql);
assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
}
#[test]
fn test_limit_one_over_a_multi_link_path_stays_a_scalar() {
let out = compile_and_emit("SELECT Person { t := (select .posts.title limit 1) }");
assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
}
#[test]
fn test_computed_multilink_can_carry_a_nested_shape() {
let out = compile_and_emit("SELECT Person { p := .posts { title } }");
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
let ShapeNode::Array { name, element, .. } = &pointers[2] else {
panic!("{:?}", pointers[2])
};
assert_eq!(name, "p");
let ShapeNode::Object {
pointers: elem_pointers,
..
} = element.as_ref()
else {
panic!()
};
assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
}
#[test]
fn test_schema_declared_backlink_computed_is_an_object_pointer() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
let post = schema.types.iter_mut().find(|t| t.name == "Post").unwrap();
post.computed = vec![ComputedDescriptor {
name: "authors".into(),
expression: ".<posts[is Person]".into(),
return_type: None,
link_target: None,
link_multi: false,
}];
let out = compile_and_emit_with("SELECT Post { authors { name } }", &schema);
assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
assert!(out.sql.contains("\"t1\".\"name\"::text"), "{}", out.sql);
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
}
#[test]
fn test_schema_declared_multilink_computed_defaults_to_ids_without_a_shape() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
schema.types[0].computed = vec![ComputedDescriptor {
name: "everything".into(),
expression: ".posts".into(),
return_type: None,
link_target: None,
link_multi: false,
}];
let out = compile_and_emit_with("SELECT Person { everything }", &schema);
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
assert!(matches!(&pointers[2], ShapeNode::Array { name, .. } if name == "everything"));
}
#[test]
fn test_schema_declared_computed_sub_select_is_a_link_pointer() {
use crate::schema::ComputedDescriptor;
let mut schema = make_schema();
schema.types[0].computed = vec![
ComputedDescriptor {
name: "recent".into(),
expression: "(select .posts order by .title desc limit 1)".into(),
return_type: None,
link_target: None,
link_multi: false,
},
ComputedDescriptor {
name: "recent_title".into(),
expression: "(select .posts order by .title desc limit 1).title".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
},
];
let out = compile_and_emit_with("SELECT Person { recent { title }, recent_title }", &schema);
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
assert!(matches!(&pointers[2], ShapeNode::Object { name, .. } if name == "recent"));
assert!(matches!(&pointers[3], ShapeNode::Scalar { name, .. } if name == "recent_title"));
assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
assert_eq!(out.sql.matches("LIMIT 1").count(), 2, "{}", out.sql);
crate::validate::validate_schema_types(&schema).expect("schema should validate");
}
#[test]
fn test_a_mutation_read_through_a_path_runs_as_a_hoisted_cte() {
let out = compile_and_emit("SELECT (INSERT Person { name := 'x' }).name");
assert!(
out.sql.contains("\"_nested_dml_0\" AS (") && out.sql.contains("INSERT INTO"),
"the mutation must run as a CTE:\n{}",
out.sql
);
assert!(
out.sql.contains("FROM \"_nested_dml_0\""),
"the projection must read the mutation's own rows:\n{}",
out.sql
);
}
#[test]
fn test_dml_sub_statement_in_expression_position_still_rejected() {
let err = compile_err("SELECT Person { x := (insert Company { name := 'c' }).name }");
assert!(err.contains("an insert cannot stand in for a value"), "{err}");
}
#[test]
fn test_sub_select_with_a_shape_in_expression_position_is_rejected() {
let err = compile_err("SELECT Person { name } filter (select .posts { title }) = 1");
assert!(err.contains("sub-select with a shape"), "{err}");
}
fn make_schema_with_through() -> SchemaDescriptor {
let id_prop = || PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("gen_random_uuid()".into()),
description: None,
default_pyql: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
};
let name_prop = || PropertyDescriptor {
name: "name".into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
description: None,
check_constraints: vec![],
default_pyql: None,
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
};
SchemaDescriptor {
types: vec![
TypeDescriptor {
name: "Person".into(),
module: "default".into(),
table: "Person".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop(), name_prop()],
links: vec![],
multilinks: vec![MultiLinkDescriptor {
name: "friends".into(),
target: "default::Person".into(),
through: Some("default::PersonFriend".into()),
nullable: false,
description: None,
default_pyql: None,
on_delete: vec![],
is_exclusive: false,
}],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "PersonFriend".into(),
module: "default".into(),
table: "PersonFriend".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop()],
links: vec![
LinkDescriptor {
name: "person".into(),
target: "default::Person".into(),
nullable: false,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
},
LinkDescriptor {
name: "friend".into(),
target: "default::Person".into(),
nullable: false,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
},
],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
}
}
#[test]
fn test_select_through_multi_link() {
let schema = make_schema_with_through();
let ast = crate::parse::parse("SELECT Person { name, friends { name } }").unwrap();
let ir = crate::ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(out.sql.contains("\"public\".\"PersonFriend\""));
assert!(out.sql.contains("\"friend\""));
assert!(out.sql.contains("\"person\""));
assert!(out.sql.contains("array_agg(ROW("));
}
#[test]
fn test_limited_multi_link_keeps_a_nested_multi_link_in_scope() {
let schema = make_schema_with_through();
let ast = crate::parse::parse("SELECT Person { friends: { name, friends { name } } limit 1 }").unwrap();
let ir = crate::ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert_eq!(out.sql.matches("array_agg(ROW(").count(), 2, "{}", out.sql);
let derived = out.sql.find("AS \"__lim\"").unwrap();
let nested = out.sql.rfind("array_agg(ROW(").unwrap();
assert!(
nested < derived,
"nested aggregate must sit inside the derived table:\n{}",
out.sql
);
}
fn make_schema_with_junction_backed_link() -> SchemaDescriptor {
let id_prop = || PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("gen_random_uuid()".into()),
description: None,
default_pyql: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
};
let name_prop = || PropertyDescriptor {
name: "name".into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
description: None,
check_constraints: vec![],
default_pyql: None,
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
};
SchemaDescriptor {
types: vec![
TypeDescriptor {
name: "Person".into(),
module: "default".into(),
table: "Person".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop(), name_prop()],
links: vec![LinkDescriptor {
name: "spouse".into(),
target: "default::Org".into(),
nullable: true,
through: Some("default::Marriage".into()),
description: None,
default_pyql: None,
is_exclusive: true,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
}],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Org".into(),
module: "default".into(),
table: "Org".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop(), name_prop()],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Marriage".into(),
module: "default".into(),
table: "Person.spouse".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![
id_prop(),
PropertyDescriptor {
name: "since".into(),
pg_type: "int8".into(),
nullable: true,
default_sql: None,
description: None,
check_constraints: vec![],
default_pyql: None,
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
},
],
links: vec![
LinkDescriptor {
name: "source".into(),
target: "default::Person".into(),
nullable: false,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
},
LinkDescriptor {
name: "target".into(),
target: "default::Org".into(),
nullable: false,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
},
],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: true,
signals: vec![],
},
],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
}
}
#[test]
fn test_select_shape_over_junction_backed_single_link() {
let schema = make_schema_with_junction_backed_link();
let ast = crate::parse::parse("SELECT Person { name, spouse { name } }").unwrap();
let ir = crate::ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
assert!(out.sql.contains("\"jt\".source"), "got:\n{}", out.sql);
assert!(out.sql.contains("\"jt\".target"), "got:\n{}", out.sql);
assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
}
#[test]
fn test_select_shape_over_junction_backed_single_link_with_link_property() {
let schema = make_schema_with_junction_backed_link();
let ast = crate::parse::parse("SELECT Person { name, spouse { name, @since } }").unwrap();
let ir = crate::ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(out.sql.contains("\"jt\".\"since\""), "got:\n{}", out.sql);
assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
}
#[test]
fn test_select_path_over_junction_backed_single_link() {
let schema = make_schema_with_junction_backed_link();
let ast = crate::parse::parse("SELECT Person.spouse { name }").unwrap();
let ir = crate::ir::compile(&ast, &schema).unwrap();
let out = emit(&ir);
assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
assert!(out.sql.contains("\"source\""), "got:\n{}", out.sql);
assert!(out.sql.contains("\"target\""), "got:\n{}", out.sql);
assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
}
#[test]
fn test_insert_with_junction_backed_single_link() {
let schema = make_schema_with_junction_backed_link();
let out = compile_and_emit_with(
"INSERT Person { name := $name, \
spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
&schema,
);
assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
assert!(out.sql.contains("\"since\""), "got:\n{}", out.sql);
assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
}
#[test]
fn test_update_replace_junction_backed_single_link() {
let schema = make_schema_with_junction_backed_link();
let out = compile_and_emit_with(
"UPDATE Person FILTER .id = $id SET { \
spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
&schema,
);
assert!(
out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
"got:\n{}",
out.sql
);
assert!(
out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
"got:\n{}",
out.sql
);
assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
assert!(out.sql.contains("NOT IN"), "got:\n{}", out.sql);
}
#[test]
fn test_unless_conflict_else_update_keeps_its_filter() {
let out = compile_and_emit(
"INSERT Person { name := 'a' } UNLESS CONFLICT ON .name \
ELSE (UPDATE Person FILTER .age < 30 SET { name := 'b' })",
);
assert!(
out.sql.contains("DO UPDATE SET") && out.sql.contains("WHERE"),
"the ELSE UPDATE's filter must survive as a DO UPDATE predicate:\n{}",
out.sql
);
assert!(
out.sql.contains("\"Person\".\"age\""),
"the predicate must read the existing row, not `excluded`:\n{}",
out.sql
);
}
#[test]
fn test_unless_conflict_else_update_without_a_filter_has_no_predicate() {
let out = compile_and_emit(
"INSERT Person { name := 'a' } UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { name := 'b' })",
);
let after_set = out.sql.split("DO UPDATE SET").nth(1).unwrap_or_default();
assert!(
!after_set.contains("WHERE"),
"got a predicate we never asked for:\n{}",
out.sql
);
}
#[test]
fn test_junction_backed_single_link_rejected_in_unless_conflict_else() {
let schema = make_schema_with_junction_backed_link();
let ast = parse::parse(
"INSERT Person { name := $name } \
UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { \
spouse := (SELECT Org FILTER .id = $oid) })",
)
.unwrap();
assert!(ir::compile(&ast, &schema).is_err());
}
#[test]
fn test_update_clear_junction_backed_single_link() {
let schema = make_schema_with_junction_backed_link();
let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := {} }", &schema);
assert!(
out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
"got:\n{}",
out.sql
);
assert!(
!out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
"clearing must not also insert:\n{}",
out.sql
);
}
#[test]
fn test_update_clear_junction_backed_single_link_with_cast_empty_set() {
let schema = make_schema_with_junction_backed_link();
let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := <Org>{} }", &schema);
assert!(
out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
"got:\n{}",
out.sql
);
assert!(
!out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
"clearing must not also insert:\n{}",
out.sql
);
}
fn make_schema_with_through_and_prop() -> SchemaDescriptor {
let id_prop = || PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("gen_random_uuid()".into()),
description: None,
default_pyql: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
};
let name_prop = || PropertyDescriptor {
name: "name".into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
description: None,
check_constraints: vec![],
default_pyql: None,
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
};
SchemaDescriptor {
types: vec![
TypeDescriptor {
name: "Product".into(),
module: "default".into(),
table: "Product".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop(), name_prop()],
links: vec![],
multilinks: vec![MultiLinkDescriptor {
name: "tags".into(),
target: "default::Tag".into(),
through: Some("default::ProductTag".into()),
nullable: false,
description: None,
default_pyql: None,
on_delete: vec![],
is_exclusive: false,
}],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Tag".into(),
module: "default".into(),
table: "Tag".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop(), name_prop()],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "ProductTag".into(),
module: "default".into(),
table: "Product.tags".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![
id_prop(),
PropertyDescriptor {
name: "weight".into(),
pg_type: "float8".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
},
],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: true,
signals: vec![],
},
],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
}
}
#[test]
fn test_link_property_in_a_links_own_filter() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with("SELECT Product { tags: { } filter @weight > 0.5 }", &schema);
assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
}
#[test]
fn test_exists_on_a_link_property() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with("SELECT Product { tags: { } filter exists @weight }", &schema);
assert!(out.sql.contains("\"jt\".\"weight\" IS NOT NULL"), "{}", out.sql);
}
#[test]
fn test_link_property_in_a_computed_link_filter() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with(
"SELECT Product { best := (select .tags filter @weight > 0.5 limit 1) { @weight } }",
&schema,
);
assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
}
#[test]
fn test_link_property_in_a_projected_sub_select_reads_the_junction() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with(
"SELECT Product { t := (select .tags filter @weight > 0.5 limit 1).id }",
&schema,
);
assert!(out.sql.contains("\"t3\".\"weight\" > (0.5::float8)"), "{}", out.sql);
}
#[test]
fn test_unknown_link_property_names_the_through_type() {
let schema = make_schema_with_through_and_prop();
let ast = parse::parse("SELECT Product { tags: { } filter @nope > 0.5 }").unwrap();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
};
assert!(err.contains("default::ProductTag") && err.contains("nope"), "{err}");
}
#[test]
fn test_link_property_outside_a_link_says_where_it_belongs() {
let schema = make_schema_with_through_and_prop();
let ast = parse::parse("SELECT Product { t := @weight }").unwrap();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
};
assert!(err.contains("is a link property"), "{err}");
}
#[test]
fn test_multilink_append_with_link_property() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with(
"UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
&schema,
);
assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
assert!(
out.sql
.contains("ON CONFLICT (\"source\", \"target\") DO UPDATE SET \"weight\" = EXCLUDED.\"weight\""),
"missing upsert conflict clause:\n{}",
out.sql
);
}
#[test]
fn test_multilink_append_union_with_different_link_property_values() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with(
"UPDATE Product FILTER .id = $id SET { \
tags += (SELECT Tag FILTER .id = $aid) { @weight := <float64>$w1 } \
union (SELECT Tag FILTER .id = $bid) { @weight := <float64>$w2 } \
}",
&schema,
);
assert!(
out.sql.contains("UNION ALL"),
"expected a UNION ALL between the two shaped targets:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("AS \"weight\"").count(),
2,
"each union branch must project its own weight:\n{}",
out.sql
);
}
#[test]
fn test_multilink_append_without_link_property_keeps_do_nothing() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with(
"UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) }",
&schema,
);
assert!(
out.sql.contains("ON CONFLICT DO NOTHING"),
"expected plain DO NOTHING when no link properties are set:\n{}",
out.sql
);
assert!(
!out.sql.contains("\"weight\""),
"unexpected weight column with no link property assignment:\n{}",
out.sql
);
}
#[test]
fn test_multilink_link_property_rejected_on_standard_junction() {
let ast = crate::parse::parse(
"UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $t) { @weight := <float64>$w } }",
).unwrap();
match crate::ir::compile(&ast, &make_schema()) {
Ok(_) => panic!("expected a compile error for link property on a Standard junction"),
Err(e) => assert!(
e.to_string().contains("Through"),
"expected a Through[...]-related error, got: {e}"
),
}
}
#[test]
fn test_multilink_link_property_rejected_on_remove() {
let schema = make_schema_with_through_and_prop();
let ast = crate::parse::parse(
"UPDATE Product FILTER .id = $id SET { tags -= (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
)
.unwrap();
match crate::ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error for link property on a remove (-=)"),
Err(e) => assert!(
e.to_string().contains("removing"),
"expected a remove-related error, got: {e}"
),
}
}
#[test]
fn test_insert_with_multilink_assignment() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with(
"INSERT Product { name := $name, tags := (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
&schema,
);
assert!(
out.sql.contains("\"_w__ids\" AS (\nINSERT INTO"),
"missing row-insert CTE:\n{}",
out.sql
);
assert!(
out.sql.contains("\"_w__ml_add_0\" AS ("),
"missing junction-append CTE:\n{}",
out.sql
);
assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
assert!(
out.sql.contains("\"_w\" AS (\n SELECT * FROM \"_w__ids\"\n)"),
"missing _w passthrough:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("WITH\n").count(),
1,
"must be a single flat top-level WITH block:\n{}",
out.sql
);
}
#[test]
fn test_user_with_binding_merges_into_a_statements_own_ctes() {
let schema = make_schema_with_through_and_prop();
for query in [
"with t := (select Tag filter .id = $tid) insert Product { name := $name, tags := t }",
"with t := (select Tag filter .id = $tid) \
update Product filter .name = $name set { tags += t }",
] {
let out = compile_and_emit_with(query, &schema);
let flat: String = out.sql.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
!flat.contains(") WITH "),
"a second WITH clause is invalid SQL:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("WITH").count(),
1,
"exactly one WITH keyword expected:\n{}",
out.sql
);
}
}
#[test]
fn test_with_bound_insert_with_multilink_assignment() {
let schema = make_schema_with_through_and_prop();
let out = compile_and_emit_with(
"with insert0 := (insert Product { name := $name, tags := (select Tag filter .id = $tid) }) select insert0",
&schema,
);
assert!(
out.sql.contains("\"insert0__ids\" AS (\nINSERT INTO"),
"missing row-insert CTE:\n{}",
out.sql
);
assert!(
out.sql.contains("\"insert0__ml_add_0\" AS ("),
"missing junction-append CTE:\n{}",
out.sql
);
assert!(
out.sql
.contains("\"insert0\" AS (\n SELECT * FROM \"insert0__ids\"\n)"),
"missing insert0 passthrough:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("WITH\n").count(),
1,
"must be a single flat top-level WITH block:\n{}",
out.sql
);
}
#[test]
fn test_with_block_cte_over_computed_global_merges_into_single_with_clause() {
let mut schema = make_schema();
schema.globals.push(GlobalDescriptor {
name: "current_user_id".into(),
module: "default".into(),
scalar_type: "std::uuid".into(),
required: false,
default_expr: None,
computed_expr: None,
});
schema.globals.push(GlobalDescriptor {
name: "current_user".into(),
module: "default".into(),
scalar_type: "Person".into(),
required: false,
default_expr: None,
computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
});
let out = compile_and_emit_with("with\n user := (select global current_user)\nselect user;", &schema);
assert_eq!(
out.sql.matches("WITH").count(),
1,
"must be a single WITH clause, got:\n{}",
out.sql
);
}
#[test]
fn test_several_mutations_in_a_free_set_each_become_a_cte() {
let schema = make_schema();
let out = compile_and_emit_with(
"select { (update Person filter .name = 'a' set { age := 1 }), \
(update Company filter .name = 'b' set { name := 'c' }) }",
&schema,
);
assert_eq!(
out.sql.matches("UPDATE").count(),
2,
"both mutations have to run, got:\n{}",
out.sql
);
assert!(out.sql.contains("WITH"), "each becomes a CTE, got:\n{}", out.sql);
}
#[test]
fn test_updating_a_binding_touches_only_its_own_rows() {
let schema = make_schema();
let out = compile_and_emit_with(
"with mine := (select Person filter .name = 'a') select { (update mine set { age := 1 }) }",
&schema,
);
assert!(
out.sql.contains("UPDATE") && out.sql.contains("= ANY(ARRAY(SELECT"),
"expected the update narrowed to the binding's rows, got:\n{}",
out.sql
);
}
#[test]
fn test_updating_a_traversal_touches_only_the_rows_it_lands_on() {
let mut schema = make_schema();
let company = schema
.types
.iter_mut()
.find(|t| t.name == "Company")
.expect("test schema has a Company type");
company.properties.push(crate::schema::PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("uuidv7()".into()),
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
let out = compile_and_emit_with(
"with mine := (select Person filter .name = 'a') select { (update mine.company set { name := 'x' }) }",
&schema,
);
assert!(
out.sql.contains("= ANY(ARRAY(SELECT"),
"expected the update narrowed to the traversal's rows, got:\n{}",
out.sql
);
assert!(
out.sql.contains("\"Company\""),
"expected the table the traversal ends on, got:\n{}",
out.sql
);
}
#[test]
fn test_aggregate_over_a_multi_valued_path_keeps_the_set_flat() {
let schema = make_schema();
let out = compile_and_emit_with(
"with a := (select Person limit 1) select { titles := array_agg(a.posts.title) }",
&schema,
);
assert!(
!out.sql.contains("array_agg(ARRAY("),
"the aggregate must take the set, not the array standing for it, got:\n{}",
out.sql
);
assert!(
out.sql.contains("array_agg(") && out.sql.contains("\"title\""),
"expected the aggregate over the traversal's own column, got:\n{}",
out.sql
);
assert!(
out.sql.contains("ROW(") && !out.sql.contains("jsonb_build_object"),
"expected a composite row, got:\n{}",
out.sql
);
}
#[test]
fn test_with_bound_root_read_by_name_inside_a_nested_select() {
let schema = make_schema();
let out = compile_and_emit_with(
"with
owner := (select Person limit 1)
select owner { posts := (select owner.posts.title) };",
&schema,
);
assert!(
out.sql.contains("WITH") && out.sql.contains("\"title\""),
"expected the nested select to traverse the binding, got:\n{}",
out.sql
);
}
#[test]
fn test_path_traversal_into_with_bound_cte_of_object_type() {
let mut schema = make_schema();
schema.globals.push(GlobalDescriptor {
name: "current_user_id".into(),
module: "default".into(),
scalar_type: "std::uuid".into(),
required: false,
default_expr: None,
computed_expr: None,
});
schema.globals.push(GlobalDescriptor {
name: "current_user".into(),
module: "default".into(),
scalar_type: "Person".into(),
required: false,
default_expr: None,
computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
});
let out = compile_and_emit_with(
"with\n user := (select global current_user)\nselect user.name;",
&schema,
);
assert!(
out.sql.contains("FROM \"user\""),
"expected path traversal from the CTE, got:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("WITH").count(),
1,
"must be a single WITH clause, got:\n{}",
out.sql
);
}
#[test]
fn test_with_bound_schema_object_property_access_in_filter_expression() {
let schema = make_schema();
let out = compile_and_emit_with(
"with\n person := (select detached Person filter .id = <uuid>$id)\n\
select Person filter .id not in person.id;",
&schema,
);
assert!(
out.sql.contains("<> ALL((SELECT") && out.sql.contains(".\"id\"") && out.sql.contains("FROM \"person\""),
"got:\n{}",
out.sql
);
}
#[test]
fn test_with_bound_schema_object_scalar_property_access() {
let schema = make_schema();
let out = compile_and_emit_with(
"with\n person := (select detached Person filter .id = <uuid>$id)\n\
select Person filter .name = person.name;",
&schema,
);
assert!(
out.sql.contains("(SELECT") && out.sql.contains(".\"name\"") && out.sql.contains("FROM \"person\""),
"got:\n{}",
out.sql
);
}
#[test]
fn test_with_bound_schema_object_link_traversal_reaches_nested_property() {
let schema = make_schema();
let out = compile_and_emit_with(
"with\n person := (select detached Person filter .id = <uuid>$id)\n\
select Company filter .name = person.company.name;",
&schema,
);
assert!(
out.sql.contains("(SELECT") && out.sql.contains("\"name\"") && out.sql.contains("\"Company\""),
"got:\n{}",
out.sql
);
}
#[test]
fn test_with_bound_schema_object_bare_link_reduces_to_id() {
let schema = make_schema();
let out = compile_and_emit_with(
"with\n person := (select detached Person filter .id = <uuid>$id)\n\
select Person filter .id = person.company;",
&schema,
);
assert!(
out.sql.contains("(SELECT") && out.sql.contains("\"id\""),
"got:\n{}",
out.sql
);
}
#[test]
fn test_with_bound_schema_object_unknown_property_suggests_a_close_match() {
let schema = make_schema();
let ast = parse::parse(
"with\n person := (select detached Person filter .id = <uuid>$id)\n\
select Person filter .name = person.nam;",
)
.unwrap();
match ir::compile(&ast, &schema) {
Err(err) => assert!(format!("{err}").contains("Did you mean 'name'"), "got: {err}"),
Ok(_) => panic!("expected a compile error"),
}
}
#[test]
fn test_with_bound_free_object_passthrough_preserves_all_fields() {
let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test;");
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected Object shape, got {:?}", out.shape.root)
};
assert_eq!(pointers.len(), 2);
assert!(matches!(&pointers[0], ShapeNode::Scalar { name, .. } if name == "test2"));
assert!(matches!(&pointers[1], ShapeNode::Scalar { name, .. } if name == "test3"));
}
#[test]
fn test_with_bound_free_object_field_access() {
let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test.test2;");
assert!(out.sql.contains("\"test2\" FROM \"test\""), "got:\n{}", out.sql);
}
#[test]
fn test_with_bound_free_object_nested_field_access_chain() {
let out =
compile_and_emit("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.foo;");
assert!(out.sql.contains("\"test3\" FROM \"test\""), "got:\n{}", out.sql);
assert!(
out.sql.contains("->'foo'"),
"expected jsonb field extraction, got:\n{}",
out.sql
);
}
#[test]
fn test_with_bound_free_object_nested_field_access_wrong_field_errors() {
let schema = make_schema();
let ast = parse::parse("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.nope;")
.unwrap();
assert!(ir::compile(&ast, &schema).is_err());
}
#[test]
fn test_nested_free_object_literal_in_computed_shape_element() {
let out = compile_and_emit("select default::Person { id, test := { foo := 'bar' } };");
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected Object shape")
};
let test_node = pointers
.iter()
.find(|p| matches!(p, ShapeNode::NamedTuple { name, .. } if name == "test"))
.unwrap_or_else(|| panic!("expected a NamedTuple shape node for 'test', got {:?}", pointers));
assert!(matches!(
test_node,
ShapeNode::NamedTuple {
is_free_object: true,
..
}
));
}
#[test]
fn test_bare_free_cte_reference_in_computed_shape_collapses_to_empty() {
let out = compile_and_emit(
"with\n test := { test2 := 1.0, test3 := 'str' }\n\
select default::Person { id, test := test };",
);
assert!(
out.sql.contains("jsonb_build_object()"),
"expected an empty free object, got:\n{}",
out.sql
);
}
#[test]
fn test_shaped_free_cte_reference_projects_fields() {
let out = compile_and_emit(
"with\n test := { test2 := 1.0, test3 := 'str' }\n\
select default::Person { id, test := test { test2 } };",
);
assert!(out.sql.contains("jsonb_build_object('test2'"), "got:\n{}", out.sql);
assert!(
!out.sql.contains("'test3'"),
"test3 should not be projected, got:\n{}",
out.sql
);
}
#[test]
fn test_insert_multilink_remove_rejected() {
let schema = make_schema_with_through_and_prop();
let ast =
crate::parse::parse("INSERT Product { name := $name, tags -= (SELECT Tag FILTER .id = $tid) }").unwrap();
match crate::ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error for `-=` on a multi-link at insert time"),
Err(e) => assert!(
e.to_string().contains("nothing to remove"),
"expected a 'nothing to remove yet' error, got: {e}"
),
}
}
#[test]
fn test_multilink_junction_info_disambiguates_self_referencing_through_type() {
let schema = make_schema_with_through();
let out = compile_and_emit_with(
"UPDATE Person FILTER .id = $id SET { friends += (SELECT Person FILTER .id = $fid) }",
&schema,
);
assert!(
out.sql.contains("(\"person\", \"friend\")"),
"expected two distinct FK columns:\n{}",
out.sql
);
assert!(
!out.sql.contains("(\"person\", \"person\")"),
"source/target collapsed to the same column:\n{}",
out.sql
);
}
#[test]
fn test_shape_descriptor_scalars() {
let out = compile_and_emit("SELECT Person { name, age }");
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
assert_eq!(pointers.len(), 4); assert!(matches!(&pointers[0], ShapeNode::Scalar { name, position: 0 } if name == "__type__"));
assert!(matches!(&pointers[1], ShapeNode::Scalar { name, position: 1 } if name == "id"));
assert!(matches!(&pointers[2], ShapeNode::Scalar { name, position: 2 } if name == "name"));
assert!(matches!(&pointers[3], ShapeNode::Scalar { name, position: 3 } if name == "age"));
}
#[test]
fn test_shape_descriptor_multi_link() {
let out = compile_and_emit("SELECT Person { name, posts { title } }");
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!()
};
assert_eq!(pointers.len(), 4);
let ShapeNode::Array {
name,
position,
element,
} = &pointers[3]
else {
panic!()
};
assert_eq!(name, "posts");
assert_eq!(*position, 3);
let ShapeNode::Object {
pointers: elem_pointers,
..
} = element.as_ref()
else {
panic!()
};
assert_eq!(elem_pointers.len(), 2);
}
#[test]
fn test_select_order_by_limit() {
let out = compile_and_emit("SELECT Person { name } ORDER BY .name ASC LIMIT 10");
assert!(out.sql.contains("ORDER BY"));
assert!(out.sql.contains("LIMIT 10"));
}
#[test]
fn test_insert_returning() {
let out = compile_and_emit("INSERT Person { name := 'Alice', age := 30 }");
assert!(out.sql.contains("INSERT INTO \"public\".\"Person\""));
assert!(out.sql.contains("RETURNING"));
assert!(out.sql.contains("'default::Person'::text"));
assert!(out.sql.contains(") AS result"));
let ShapeNode::Object {
cardinality, pointers, ..
} = &out.shape.root
else {
panic!()
};
assert_eq!(*cardinality, Cardinality::Required);
assert!(
pointers
.iter()
.any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "id"))
);
assert!(
!pointers
.iter()
.any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "name"))
);
}
#[test]
fn test_update_returning() {
let out = compile_and_emit("UPDATE Person FILTER .name = $name SET { age := 31 }");
assert!(out.sql.contains("UPDATE \"public\".\"Person\""));
assert!(out.sql.contains("SET"));
assert!(out.sql.contains("RETURNING"));
assert!(out.sql.contains("'default::Person'::text"));
assert!(!out.sql.contains("\"name\"::text"), "bare UPDATE must not return name");
}
#[test]
fn test_update_set_tuple_param_cast_uses_direct_jsonb_cast_not_to_jsonb() {
let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := <tuple<x: float64>>$val }");
assert!(
out.sql.contains(")::jsonb"),
"expected a direct ::jsonb cast, got:\n{}",
out.sql
);
assert!(
!out.sql.contains("to_jsonb($"),
"must not pass a bare param straight into to_jsonb(): got:\n{}",
out.sql
);
}
#[test]
fn test_empty_set_cast_to_object_type_clears_optional_link() {
let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { company := <default::Company>{} }");
assert!(out.sql.contains("\"company_id\" = NULL"), "got:\n{}", out.sql);
}
#[test]
fn test_an_aggregate_over_a_set_literal_inside_a_shape() {
let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := min({<int64>100, .age}) }");
assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
assert!(
out.sql.contains("UNION ALL"),
"both operands must reach the aggregate:\n{}",
out.sql
);
}
#[test]
fn test_an_aggregate_over_a_set_literal_reads_the_subject() {
let out = compile_and_emit("SELECT Person { capped := min({<int64>100, .age}) }");
assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
assert!(
out.sql.contains("\"age\""),
"the relative path must reach the column:\n{}",
out.sql
);
}
#[test]
fn test_an_aggregate_over_a_union_inside_a_shape() {
let out = compile_and_emit("SELECT Person { capped := min((<int64>100 union .age)) }");
assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
assert!(
out.sql.contains("UNION ALL"),
"both arms must reach the aggregate:\n{}",
out.sql
);
}
#[test]
fn test_an_aggregate_over_a_union_of_three_arms_flattens() {
let out = compile_and_emit("SELECT Person { capped := min((<int64>1 union <int64>2 union .age)) }");
assert_eq!(
out.sql.matches("UNION ALL").count(),
2,
"three arms means two joins:\n{}",
out.sql
);
}
#[test]
fn test_an_aggregate_over_an_empty_set_emits_no_row_source() {
for query in ["SELECT Person { capped := min({}) }", "SELECT min({})"] {
let ast = parse::parse(query).expect("parse failed");
let sql = ir::compile(&ast, &make_schema())
.map(|ir| emit(&ir).sql)
.unwrap_or_default();
assert!(
!sql.contains("FROM () AS"),
"{query} emitted an empty row source:\n{sql}"
);
}
}
#[test]
fn test_delete_returning() {
let out = compile_and_emit("DELETE Person FILTER .id = $id");
assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""));
assert!(out.sql.contains("RETURNING"));
assert!(out.sql.contains("'default::Person'::text"));
assert!(!out.sql.contains("\"name\"::text"), "bare DELETE must not return name");
}
#[test]
fn test_select_over_insert() {
let out = compile_and_emit("SELECT (INSERT Person { name := $name, age := $age }) { id, name }");
assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
assert!(out.sql.contains("INSERT INTO"));
assert!(out.sql.contains("RETURNING *"));
assert!(out.sql.contains("'default::Person'::text"));
assert!(out.sql.contains("\"name\"::text"));
}
#[test]
fn test_select_over_update() {
let out = compile_and_emit("SELECT (UPDATE Person FILTER .id = $id SET { name := $name }) { id, name }");
assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
assert!(out.sql.contains("UPDATE"));
assert!(out.sql.contains("RETURNING \"t1\".*"), "{}", out.sql);
assert!(out.sql.contains("\"name\"::text"));
}
#[test]
fn test_insert_user_specified_id_denied_by_default() {
let schema = make_schema();
let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
match ir::compile_with_config(&ast, &schema, &ir::SessionConfig::default()) {
Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
Ok(_) => panic!("expected id assignment to be denied by default"),
}
}
#[test]
fn test_insert_user_specified_id_allowed_when_configured() {
let schema = make_schema();
let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
let config = ir::SessionConfig {
allow_user_specified_id: true,
};
let ir_out = ir::compile_with_config(&ast, &schema, &config)
.expect("expected id assignment to be allowed with allow_user_specified_id");
let out = emit(&ir_out);
assert!(out.sql.contains("INSERT INTO"));
}
#[test]
fn test_update_user_specified_id_denied_even_when_configured() {
let schema = make_schema();
let ast = parse::parse("UPDATE Person FILTER .name = $name SET { id := <uuid>$id }").unwrap();
let config = ir::SessionConfig {
allow_user_specified_id: true,
};
match ir::compile_with_config(&ast, &schema, &config) {
Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
Ok(_) => panic!("expected UPDATE to always deny reassigning id"),
}
}
#[test]
fn test_select_over_update_multilink_only() {
let out = compile_and_emit(
"SELECT (UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $title) }) { id, name }",
);
assert!(
out.sql.contains("\"_dml__ml_add_0\""),
"missing junction-append CTE:\n{}",
out.sql
);
assert!(out.sql.contains("INSERT INTO"), "missing junction INSERT:\n{}", out.sql);
assert!(
out.sql.contains("\"_dml__ids\" AS (\nSELECT"),
"expected SELECT-based _ids CTE:\n{}",
out.sql
);
assert!(
!out.sql.contains("SET\n\nWHERE") && !out.sql.contains("SET \nWHERE"),
"empty SET clause regression:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("WITH\n").count(),
1,
"junction CTE must not be nested in a second WITH:\n{}",
out.sql
);
assert!(
out.sql.contains("\"_dml\" AS (\n SELECT * FROM \"_dml__ids\"\n)"),
"missing _dml passthrough:\n{}",
out.sql
);
}
#[test]
fn test_select_over_update_scalar_and_multilink() {
let out = compile_and_emit(
"SELECT (UPDATE Person FILTER .id = $id SET { name := $name, posts += (SELECT Post FILTER .title = $title) }) { id, name }",
);
assert!(
out.sql.contains("\"_dml__ml_add_0\""),
"missing junction-append CTE:\n{}",
out.sql
);
assert!(
out.sql.contains("\"_dml__ids\" AS (\nUPDATE"),
"expected UPDATE-based _ids CTE:\n{}",
out.sql
);
assert!(
out.sql.contains("\"name\" = "),
"missing scalar SET assignment:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("WITH\n").count(),
1,
"junction CTE must not be nested in a second WITH:\n{}",
out.sql
);
}
#[test]
fn test_with_bound_insert_and_multilink_update_forward_ref() {
let out = compile_and_emit(
"with insert0 := (insert Post { title := $title }), update0 := (update Person filter .id = $id set { posts += (select insert0) }) select { insert0, update0 }",
);
assert!(
out.sql.contains("\"insert0\" AS (\n INSERT INTO"),
"missing insert0 CTE:\n{}",
out.sql
);
assert!(
out.sql.contains("\"update0__ml_add_0\""),
"missing junction-append CTE for update0:\n{}",
out.sql
);
assert!(
out.sql.contains("\"update0__ids\" AS (\nSELECT"),
"expected SELECT-based update0 ids CTE (no scalar changes):\n{}",
out.sql
);
assert!(
out.sql
.contains("\"update0\" AS (\n SELECT * FROM \"update0__ids\"\n)"),
"missing update0 passthrough:\n{}",
out.sql
);
assert_eq!(
out.sql.matches("WITH\n").count(),
1,
"must be a single flat top-level WITH block:\n{}",
out.sql
);
}
#[test]
fn test_select_over_delete() {
let out = compile_and_emit("SELECT (DELETE Person FILTER .id = $id) { id, name }");
assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
assert!(out.sql.contains("DELETE FROM"));
assert!(out.sql.contains("RETURNING *"));
assert!(out.sql.contains("\"name\"::text"));
}
fn make_schema_with_rewrite() -> SchemaDescriptor {
use crate::schema::RewriteEntry;
let mut schema = make_schema();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
person.properties.push(PropertyDescriptor {
name: "slug".into(),
pg_type: "text".into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![
RewriteEntry {
on: 1,
handler: "str_lower(.name)".into(),
}, RewriteEntry {
on: 2,
handler: "str_lower(.name)".into(),
}, ],
tuple_members: None,
column_type: None,
});
schema
}
#[test]
fn a_link_assigned_from_a_select_keeps_its_clauses() {
let out = compile_and_emit(
"INSERT Person { name := 'a', age := 1, company := (SELECT Company ORDER BY .name LIMIT 1) }",
);
assert!(
out.sql.contains("ORDER BY") && out.sql.contains("LIMIT 1"),
"{}",
out.sql
);
}
#[test]
fn a_rewrite_leaves_the_statement_alone() {
let schema = make_schema_with_rewrite();
for query in [
"INSERT Person { name := $name, age := 30 }",
"UPDATE Person FILTER .id = $id SET { name := $name }",
] {
let out = compile_and_emit_with(query, &schema);
assert!(!out.sql.contains("\"slug\""), "{}", out.sql);
}
}
#[test]
fn a_rewrite_reads_the_row_being_written() {
let schema = make_schema_with_rewrite();
let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
let [assignment] = assignments.as_slice() else {
panic!("one rewrite on insert")
};
assert_eq!(assignment.column, "slug");
assert!(assignment.sql.contains("NEW.\"name\""), "{}", assignment.sql);
}
#[test]
fn rewrites_become_before_triggers() {
let ddl = crate::export::export_schema(&make_schema_with_rewrite()).unwrap();
for event in ["BEFORE INSERT", "BEFORE UPDATE"] {
assert!(
ddl.contains(&format!("{event} ON \"public\".\"Person\"")),
"{event}:\n{ddl}"
);
}
assert!(ddl.contains("NEW.\"slug\" := _pylon_rewrites.\"v0\";"), "{ddl}");
}
#[test]
fn a_rewrite_walking_a_link_starts_from_the_new_row() {
use crate::schema::RewriteEntry;
let mut schema = make_schema_with_rewrite();
let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
let slug = person.properties.iter_mut().find(|p| p.name == "slug").unwrap();
slug.rewrites = vec![RewriteEntry {
on: 1,
handler: "<str>(.company is Company) ++ .company.name".into(),
}];
let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
let sql = &assignments[0].sql;
assert!(!sql.contains("\"public\".\"Person\""), "{sql}");
assert!(
sql.contains("(SELECT (NEW).*)") && sql.contains("NEW.\"company_id\""),
"{sql}"
);
}
#[test]
fn test_unless_conflict_do_nothing() {
let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT");
assert!(out.sql.contains("ON CONFLICT DO NOTHING"));
}
#[test]
fn test_unless_conflict_on_do_nothing() {
let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT ON .name");
assert!(out.sql.contains("ON CONFLICT (\"name\") DO NOTHING"));
}
#[test]
fn test_unless_conflict_do_update() {
let out = compile_and_emit(
"INSERT Person { name := $name, age := $age } \
UNLESS CONFLICT ON .name \
ELSE (UPDATE Person SET { age := $age })",
);
assert!(out.sql.contains("ON CONFLICT (\"name\") DO UPDATE SET"));
assert!(out.sql.contains("\"age\" = $2"));
assert!(!out.sql.contains("DO NOTHING"));
}
#[test]
fn test_unless_conflict_else_update_self_reference_is_qualified() {
let out = compile_and_emit(
"INSERT Person { name := $name, age := 0 } \
UNLESS CONFLICT ON .name \
ELSE (UPDATE Person SET { age := .age + 1 })",
);
assert!(
out.sql.contains("\"Person\".\"age\""),
"self-reference must be qualified with the table's own name, got:\n{}",
out.sql
);
assert!(
!out.sql.contains("SET \"age\" = (\"age\""),
"must not emit an unqualified (ambiguous) self-reference, got:\n{}",
out.sql
);
}
#[test]
fn test_unless_conflict_do_update_no_on() {
let out = compile_and_emit(
"INSERT Person { name := $name } \
UNLESS CONFLICT \
ELSE (UPDATE Person SET { age := 0 })",
);
assert!(out.sql.contains("ON CONFLICT DO UPDATE SET"));
assert!(out.sql.contains("\"age\" = 0"));
}
#[test]
fn test_select_over_select() {
let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name }");
assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
assert!(out.sql.contains("SELECT *"));
assert!(out.sql.contains("FROM \"public\".\"Person\""));
assert!(out.sql.contains("WHERE"));
assert!(out.sql.contains("'default::Person'::text"));
assert!(out.sql.contains("\"name\"::text"));
}
#[test]
fn test_select_over_select_with_outer_filter() {
let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name } FILTER .name = $name");
assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
assert!(out.sql.contains("SELECT *"));
assert_eq!(out.sql.matches("WHERE").count(), 2);
assert!(out.sql.contains("$1"));
}
#[test]
fn test_insert_link_subquery() {
let out = compile_and_emit("INSERT Person { name := $name, company := (SELECT Company FILTER .name = $co) }");
assert!(out.sql.contains("\"company_id\""));
assert!(out.sql.contains("SELECT"));
assert!(out.sql.contains("\"id\""));
assert!(out.sql.contains("FROM \"public\".\"Company\""));
assert!(out.sql.contains("$2")); }
#[test]
fn test_update_link_subquery() {
let out =
compile_and_emit("UPDATE Person FILTER .id = $id SET { company := (SELECT Company FILTER .name = $co) }");
assert!(out.sql.contains("\"company_id\""));
assert!(out.sql.contains("SELECT"));
assert!(out.sql.contains("FROM \"public\".\"Company\""));
}
#[test]
fn test_insert_link_value_from_nested_insert_hoists_a_with_cte() {
let out = compile_and_emit(
"INSERT Person { name := 'Alice', company := (select (insert Company { name := 'Acme' }) { id }) }",
);
assert!(
out.sql.starts_with("WITH"),
"expected a WITH-hoisted CTE, got:\n{}",
out.sql
);
assert!(
out.sql.contains("INSERT INTO \"public\".\"Company\""),
"expected the nested insert to be its own CTE, got:\n{}",
out.sql,
);
assert!(
out.sql.contains("\"company_id\") SELECT") && out.sql.contains(".\"id\" FROM"),
"expected the outer insert to switch from VALUES to SELECT ... FROM <cte>, got:\n{}",
out.sql,
);
assert!(
!out.sql.contains("FROM \"public\".\"Company\" AS"),
"must not select from the real Company table, got:\n{}",
out.sql
);
}
#[test]
fn test_update_link_value_from_nested_insert_hoists_a_with_cte_and_adds_from_clause() {
let out = compile_and_emit(
"UPDATE Person FILTER .id = $id SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
);
assert!(
out.sql.starts_with("WITH"),
"expected a WITH-hoisted CTE, got:\n{}",
out.sql
);
assert!(
out.sql.contains("INSERT INTO \"public\".\"Company\""),
"expected the nested insert to be its own CTE, got:\n{}",
out.sql,
);
assert!(
out.sql.contains("\nFROM \""),
"expected a FROM clause referencing the nested CTE, got:\n{}",
out.sql
);
assert!(out.sql.contains("SET \"company_id\" = "), "got:\n{}", out.sql);
}
#[test]
fn test_update_link_value_from_nested_insert_combines_with_multilink_mutation() {
let out = compile_and_emit(
"UPDATE Person FILTER .id = $id SET { \
company := (select (insert Company { name := 'Acme' }) { id }), \
posts += (SELECT Post FILTER .title = $t) \
}",
);
assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
assert!(
out.sql.contains("INSERT INTO \"public\".\"Company\""),
"expected the nested insert to be its own CTE, got:\n{}",
out.sql,
);
assert!(
out.sql.contains("\"_ids\" AS (\nUPDATE") && out.sql.contains("\nFROM \""),
"expected the _ids UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
out.sql,
);
assert!(
out.sql.contains("\"_ml_add_0\""),
"expected the junction-append CTE to still be present, got:\n{}",
out.sql
);
}
#[test]
fn test_update_link_value_from_nested_insert_combines_with_interface_target() {
fn id_prop() -> PropertyDescriptor {
PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("uuidv7()".into()),
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
}
}
fn text_prop(name: &str) -> PropertyDescriptor {
PropertyDescriptor {
name: name.into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
}
}
fn company_link() -> LinkDescriptor {
LinkDescriptor {
name: "company".into(),
target: "default::Company".into(),
nullable: true,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
}
}
let schema = SchemaDescriptor {
types: vec![
TypeDescriptor {
name: "Company".into(),
module: "default".into(),
table: "Company".into(),
abstract_: false,
materialized: true,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop(), text_prop("name")],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Account".into(),
module: "default".into(),
table: "Account".into(),
abstract_: true,
materialized: true,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![id_prop(), text_prop("email")],
links: vec![company_link()],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Individual".into(),
module: "default".into(),
table: "Individual".into(),
abstract_: false,
materialized: true,
description: None,
parents: vec![],
interfaces: vec!["default::Account".into()],
bases: vec![],
properties: vec![id_prop(), text_prop("email"), text_prop("first_name")],
links: vec![company_link()],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
};
let out = compile_and_emit_with(
"UPDATE Account FILTER .email = $email \
SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
&schema,
);
assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
assert!(
out.sql.contains("INSERT INTO \"public\".\"Company\""),
"expected the nested insert to be its own CTE, got:\n{}",
out.sql,
);
assert!(
out.sql.contains("UPDATE \"public\".\"Individual\"") && out.sql.contains("\nFROM \"_nested_dml_0\""),
"expected the per-implementor UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
out.sql,
);
}
#[test]
fn test_a_backlink_narrowed_to_a_supertype() {
let mut schema = interface_link_schema();
schema
.types
.iter_mut()
.find(|t| t.name == "Account")
.expect("the helper declares Account")
.links
.clear();
let out = compile_and_emit_with("SELECT Company { owners := .<company[is Account] { email } }", &schema);
assert!(
out.sql.contains("\"public\".\"Individual\""),
"the backlink must read the type that declares it:\n{}",
out.sql
);
}
#[test]
fn a_path_through_a_backlink_narrowed_to_a_supertype() {
let mut schema = interface_link_schema();
schema
.types
.iter_mut()
.find(|t| t.name == "Account")
.expect("the helper declares Account")
.links
.clear();
let out = compile_and_emit_with(
"SELECT Company { name } FILTER EXISTS (SELECT .<company[is Account])",
&schema,
);
assert!(out.sql.contains("\"public\".\"Individual\""), "{}", out.sql);
}
fn interface_link_schema() -> SchemaDescriptor {
fn id_prop() -> PropertyDescriptor {
PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
}
}
fn text_prop(name: &str) -> PropertyDescriptor {
PropertyDescriptor {
name: name.into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
}
}
fn link(name: &str, target: &str) -> LinkDescriptor {
LinkDescriptor {
name: name.into(),
target: target.into(),
nullable: true,
through: None,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
}
}
fn ty(
name: &str,
abstract_: bool,
interfaces: Vec<String>,
properties: Vec<PropertyDescriptor>,
links: Vec<LinkDescriptor>,
) -> TypeDescriptor {
TypeDescriptor {
name: name.into(),
module: "default".into(),
table: name.into(),
abstract_,
materialized: true,
description: None,
parents: vec![],
interfaces,
bases: vec![],
properties,
links,
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
}
}
SchemaDescriptor {
types: vec![
ty("Company", false, vec![], vec![id_prop(), text_prop("name")], vec![]),
ty(
"Account",
true,
vec![],
vec![id_prop(), text_prop("email")],
vec![link("company", "default::Company")],
),
ty(
"Individual",
false,
vec!["default::Account".into()],
vec![id_prop(), text_prop("email")],
vec![link("company", "default::Company")],
),
ty(
"Token",
false,
vec![],
vec![id_prop(), text_prop("value")],
vec![link("account", "default::Account")],
),
],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
}
}
fn two_hop_interface_schema() -> SchemaDescriptor {
let mut schema = interface_link_schema();
let token = schema
.types
.iter()
.find(|t| t.name == "Token")
.expect("the helper declares Token")
.clone();
let mut connector = token.clone();
connector.name = "Connector".into();
connector.table = "Connector".into();
connector.links[0].name = "provider".into();
let mut installation = token;
installation.name = "Installation".into();
installation.table = "Installation".into();
installation.links[0].name = "connector".into();
installation.links[0].target = "default::Connector".into();
schema.types.push(connector);
schema.types.push(installation);
schema
}
#[test]
fn a_narrowed_two_hop_walk_off_a_pinned_row_is_one_value() {
let out = compile_and_emit_with(
"SELECT { s := ((SELECT Installation FILTER .id = <uuid>$0).connector.provider[is Individual].email) ?? 'y' }",
&two_hop_interface_schema(),
);
assert!(
!out.sql.contains("ARRAY(SELECT"),
"expected a value, not a set:\n{}",
out.sql
);
assert!(
out.sql.contains("COALESCE("),
"the default must stay a scalar one:\n{}",
out.sql
);
}
#[test]
fn a_narrowed_two_hop_walk_off_an_unpinned_row_stays_a_set() {
let out = compile_and_emit_with(
"SELECT { s := (SELECT Installation FILTER .value = 'x').connector.provider[is Individual].email }",
&two_hop_interface_schema(),
);
assert!(
out.sql.contains("ARRAY(SELECT"),
"a walk off many rows is a set:\n{}",
out.sql
);
}
#[test]
fn a_walk_ending_in_a_type_intersection_is_valid_in_a_filter() {
let out = compile_and_emit_with(
"SELECT Token { id } FILTER EXISTS .account[is Individual]",
&interface_link_schema(),
);
assert!(
out.sql.contains("\"public\".\"Individual\""),
"the narrowing must reach the implementor's table:\n{}",
out.sql
);
assert!(
out.sql.contains("IS NOT NULL"),
"exists over the narrowed walk is a nullness test:\n{}",
out.sql
);
}
#[test]
fn a_walk_ending_in_a_type_intersection_is_valid_off_a_binding() {
let out = compile_and_emit_with(
"WITH t := (SELECT Token LIMIT 1) SELECT { e := EXISTS t.account[is Individual] }",
&interface_link_schema(),
);
assert!(
out.sql.contains("\"public\".\"Individual\""),
"the narrowing must reach the implementor's table:\n{}",
out.sql
);
}
#[test]
fn test_reading_a_link_through_an_interface_target() {
let schema = interface_link_schema();
let out = compile_and_emit_with("SELECT Token { account: { email, company: { name } } }", &schema);
assert!(
out.sql
.contains("\"email\", \"company_id\" FROM \"public\".\"Individual\""),
"the fanned-out interface must carry its own link columns:\n{}",
out.sql
);
}
#[test]
fn test_computed_pointer_in_shape_emits_expression() {
let mut schema = make_schema();
schema.types[0].computed.push(crate::schema::ComputedDescriptor {
name: "upper_name".into(),
expression: "str_upper(.name)".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
});
let out = compile_and_emit_with("SELECT Person { upper_name }", &schema);
assert!(
out.sql.to_lowercase().contains("upper"),
"expected upper() in SQL, got:\n{}",
out.sql
);
}
fn make_interface_schema() -> SchemaDescriptor {
fn id_prop() -> PropertyDescriptor {
PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("uuidv7()".into()),
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
}
}
SchemaDescriptor {
types: vec![
TypeDescriptor {
name: "Account".into(),
module: "default".into(),
table: "Account".into(),
abstract_: true,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![
id_prop(),
PropertyDescriptor {
name: "email".into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
},
],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
TypeDescriptor {
name: "Individual".into(),
module: "default".into(),
table: "Individual".into(),
abstract_: false,
materialized: true,
description: None,
parents: vec![],
interfaces: vec!["default::Account".into()],
bases: vec![],
properties: vec![
id_prop(),
PropertyDescriptor {
name: "first_name".into(),
pg_type: "text".into(),
nullable: false,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
},
],
links: vec![],
multilinks: vec![],
computed: vec![crate::schema::ComputedDescriptor {
name: "full_name".into(),
expression: "str_upper(.first_name)".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
}],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
},
],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
}
}
#[test]
fn test_type_intersection_splat_includes_concrete_computed_pointers() {
let schema = make_interface_schema();
let out = compile_and_emit_with("SELECT Account { *, [is Individual].* }", &schema);
assert!(
out.sql.to_lowercase().contains("upper"),
"expected the concrete type's computed pointer (str_upper(...)) in the shape, got:\n{}",
out.sql
);
assert!(
out.sql.contains("\"first_name\""),
"expected the concrete type's stored property too, got:\n{}",
out.sql
);
}
#[test]
fn test_enum_array_property_casts_to_text_array_and_shapes_as_a_list() {
let mut schema = make_schema();
schema.enums.push(crate::schema::EnumDescriptor {
name: "Gender".into(),
module: "default".into(),
members: vec!["Male".into(), "Female".into()],
});
schema.types[0].properties.push(crate::schema::PropertyDescriptor {
name: "genders".into(),
pg_type: r#""default"."Gender"[]"#.into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
let out = compile_and_emit_with("SELECT Person { genders }", &schema);
assert!(
out.sql.contains(r#""genders"::text[]"#),
"expected a text[] cast, got:\n{}",
out.sql
);
let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected Object shape, got {:?}", out.shape.root)
};
let genders = pointers
.iter()
.find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "genders"))
.unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
let crate::query::ShapeNode::Array { element, .. } = genders else {
unreachable!()
};
assert!(
matches!(
element.as_ref(),
crate::query::ShapeNode::Enum { enum_type, position: 0, .. }
if enum_type == "default::Gender"
),
"expected the elements to be enum-shaped, got {element:?}",
);
}
#[test]
fn test_scalar_enum_property_still_casts_to_a_single_text() {
let mut schema = make_schema();
schema.enums.push(crate::schema::EnumDescriptor {
name: "Gender".into(),
module: "default".into(),
members: vec!["Male".into(), "Female".into()],
});
schema.types[0].properties.push(crate::schema::PropertyDescriptor {
name: "gender".into(),
pg_type: r#""default"."Gender""#.into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
let out = compile_and_emit_with("SELECT Person { gender }", &schema);
assert!(
out.sql.contains(r#""gender"::text"#) && !out.sql.contains(r#""gender"::text[]"#),
"expected a plain text cast, got:\n{}",
out.sql
);
let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected Object shape")
};
assert!(
pointers.iter().any(|node| matches!(
node,
crate::query::ShapeNode::Enum { name, enum_type, .. }
if name == "gender" && enum_type == "default::Gender"
)),
"expected an Enum-shaped pointer, got {pointers:?}",
);
}
#[test]
fn test_type_intersection_splat_keeps_enum_pointers_enum_shaped() {
let mut schema = make_interface_schema();
schema.enums.push(crate::schema::EnumDescriptor {
name: "Gender".into(),
module: "default".into(),
members: vec!["Male".into(), "Female".into()],
});
let individual = schema
.types
.iter_mut()
.find(|t| t.name == "Individual")
.expect("the interface schema has an Individual type");
individual.properties.push(crate::schema::PropertyDescriptor {
name: "gender".into(),
pg_type: r#""default"."Gender""#.into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
});
let out = compile_and_emit_with("SELECT Account { [is Individual].* }", &schema);
let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected Object shape, got {:?}", out.shape.root)
};
assert!(
pointers.iter().any(|node| matches!(
node,
crate::query::ShapeNode::Enum { name, enum_type, .. }
if name == "gender" && enum_type == "default::Gender"
)),
"expected the enum property to stay enum-shaped through the intersection, got {pointers:?}",
);
}
#[test]
fn test_shallow_splat_leaves_object_valued_computeds_to_the_deep_form() {
let mut schema = make_schema();
schema.types[0].computed.push(crate::schema::ComputedDescriptor {
name: "authors".into(),
expression: ".posts".into(),
return_type: None,
link_target: None,
link_multi: false,
});
schema.types[0].computed.push(crate::schema::ComputedDescriptor {
name: "age_next".into(),
expression: ".age + 1".into(),
return_type: Some("int8".into()),
link_target: None,
link_multi: false,
});
let shallow = compile_and_emit_with("SELECT Person { * }", &schema);
let crate::query::ShapeNode::Object { pointers, .. } = &shallow.shape.root else {
panic!("expected Object shape")
};
let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
assert!(
names.contains(&"age_next"),
"a computed property belongs in `*`: {names:?}"
);
assert!(!names.contains(&"authors"), "a computed link does not: {names:?}");
let deep = compile_and_emit_with("SELECT Person { ** }", &schema);
let crate::query::ShapeNode::Object { pointers, .. } = &deep.shape.root else {
panic!("expected Object shape")
};
let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
assert!(
names.contains(&"authors"),
"`**` includes links, computed ones included: {names:?}"
);
}
fn shape_pointer_name(node: &crate::query::ShapeNode) -> &str {
use crate::query::ShapeNode;
match node {
ShapeNode::Scalar { name, .. }
| ShapeNode::Enum { name, .. }
| ShapeNode::NamedTuple { name, .. }
| ShapeNode::Object { name, .. }
| ShapeNode::Array { name, .. } => name,
_ => "",
}
}
#[test]
fn test_any_over_a_multilink_comparison_does_not_warn() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER any(.posts.title = 'x')").unwrap();
let ir = ir::compile(&ast, &schema).unwrap();
assert!(
!ir.warnings.iter().any(|w| w.contains("FILTER clause")),
"any() states the intent, so nothing should warn: {:?}",
ir.warnings
);
}
#[test]
fn test_bare_multilink_comparison_still_warns() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .posts.title = 'x'").unwrap();
let ir = ir::compile(&ast, &schema).unwrap();
assert!(
ir.warnings.iter().any(|w| w.contains("FILTER clause")),
"an unwrapped set-valued comparison still warns: {:?}",
ir.warnings
);
}
#[test]
fn test_link_to_an_interface_expands_over_its_implementors() {
let mut schema = make_interface_schema();
let account = schema
.types
.iter_mut()
.find(|t| t.name == "Account")
.expect("the interface schema has an Account type");
account.materialized = true;
let mut organization = schema
.types
.iter()
.find(|t| t.name == "Individual")
.expect("the interface schema has an Individual type")
.clone();
organization.name = "Organization".into();
organization.table = "Organization".into();
organization.computed.clear();
schema.types.push(organization);
let owner = TypeDescriptor {
name: "Note".into(),
module: "default".into(),
table: "Note".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![],
links: vec![LinkDescriptor {
name: "owner".into(),
target: "default::Account".into(),
nullable: true,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
through: None,
}],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
};
schema.types.push(owner);
let out = compile_and_emit_with("SELECT Note { owner: { id } }", &schema);
assert!(
out.sql.contains("UNION ALL"),
"the link's target should expand over its implementors, got:\n{}",
out.sql
);
assert!(
out.sql.contains("'default::Individual'::text AS \"__type__\""),
"each branch should carry its own discriminator, got:\n{}",
out.sql
);
assert!(
!out.sql.contains("FROM \"default\".\"Account\" AS"),
"the interface's own view should no longer be read directly, got:\n{}",
out.sql
);
}
fn make_two_implementor_schema() -> SchemaDescriptor {
let mut schema = make_interface_schema();
let mut organization = schema
.types
.iter()
.find(|t| t.name == "Individual")
.expect("the interface schema has an Individual type")
.clone();
organization.name = "Organization".into();
organization.table = "Organization".into();
organization.computed.clear();
schema.types.push(organization);
schema
}
fn assert_rows_carry_their_concrete_type(query: &str) {
let out = compile_and_emit_with(query, &make_two_implementor_schema());
assert!(
!out.sql.contains("'default::Account'::text"),
"rows are labelled with the interface instead of their own type:\n{}",
out.sql
);
assert!(
out.sql.contains("'default::Individual'::text AS \"__type__\""),
"the rows should come from the implementors, each tagging its own:\n{}",
out.sql
);
}
#[test]
fn an_aggregated_select_of_an_interface_carries_the_concrete_type() {
assert_rows_carry_their_concrete_type("SELECT (xs := array_agg((SELECT Account { id })))");
}
#[test]
fn an_aggregated_binding_of_an_interface_carries_the_concrete_type() {
assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (xs := array_agg((SELECT a { id })))");
}
#[test]
fn a_single_object_read_off_a_binding_carries_the_concrete_type() {
assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (x := (SELECT a { id } LIMIT 1))");
}
#[test]
fn grouped_elements_of_an_interface_carry_the_concrete_type() {
assert_rows_carry_their_concrete_type("group Account { id } by .email");
}
fn make_note_schema() -> SchemaDescriptor {
fn link_to(name: &str, target: &str) -> LinkDescriptor {
LinkDescriptor {
name: name.into(),
target: target.into(),
nullable: true,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
through: None,
}
}
let mut schema = make_two_implementor_schema();
for account in schema
.types
.iter_mut()
.filter(|t| ["Account", "Individual", "Organization"].contains(&t.name.as_str()))
{
account.links.push(link_to("pinned", "default::Note"));
}
schema.types.push(TypeDescriptor {
name: "Note".into(),
module: "default".into(),
table: "Note".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![],
links: vec![link_to("owner", "default::Account")],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
});
schema
}
#[test]
fn a_backlink_to_an_interface_carries_the_concrete_type() {
let out = compile_and_emit_with(
"SELECT Note { pinners := .<pinned[is Account] { id } }",
&make_note_schema(),
);
assert!(
!out.sql.contains("'default::Account'::text"),
"the backlink's rows are labelled with the interface instead of their own type:\n{}",
out.sql
);
}
#[test]
fn a_link_reached_through_a_deep_splat_carries_the_concrete_type() {
let out = compile_and_emit_with("SELECT Note { ** }", &make_note_schema());
assert!(
!out.sql.contains("'default::Account'::text"),
"the splatted link's rows are labelled with the interface instead of their own type:\n{}",
out.sql
);
}
#[test]
fn test_path_traversal_onto_an_interface_carries_the_concrete_type() {
let mut schema = make_interface_schema();
let account = schema
.types
.iter_mut()
.find(|t| t.name == "Account")
.expect("the interface schema has an Account type");
account.materialized = true;
let mut organization = schema
.types
.iter()
.find(|t| t.name == "Individual")
.expect("the interface schema has an Individual type")
.clone();
organization.name = "Organization".into();
organization.table = "Organization".into();
organization.computed.clear();
schema.types.push(organization);
schema.types.push(TypeDescriptor {
name: "Note".into(),
module: "default".into(),
table: "Note".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![],
links: vec![LinkDescriptor {
name: "owner".into(),
target: "default::Account".into(),
nullable: true,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
through: None,
}],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
});
let out = compile_and_emit_with("SELECT Note.owner { id }", &schema);
assert!(
out.sql.contains("UNION ALL") && out.sql.contains("'default::Individual'::text AS \"__type__\""),
"the traversal's target should fan out over implementors, got:\n{}",
out.sql
);
assert!(
!out.sql.contains("'default::Account'::text,"),
"the row's type should be read off the row, not fixed to the interface, got:\n{}",
out.sql
);
}
#[test]
fn test_computed_multilink_chain_comes_back_as_rows() {
let mut schema = make_schema();
schema.types[0].computed.push(crate::schema::ComputedDescriptor {
name: "coauthors".into(),
expression: ".posts.author".into(),
return_type: None,
link_target: None,
link_multi: false,
});
let post = schema
.types
.iter_mut()
.find(|t| t.name == "Post")
.expect("test schema has a Post type");
post.links.push(LinkDescriptor {
name: "author".into(),
target: "default::Person".into(),
nullable: true,
description: None,
default_pyql: None,
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
through: None,
});
let out = compile_and_emit_with("SELECT Person { coauthors: { name } }", &schema);
assert!(
out.sql.contains("ARRAY(SELECT"),
"the chain should aggregate into an array, got:\n{}",
out.sql
);
assert!(
out.sql.contains("\"name\""),
"the requested sub-shape should survive, got:\n{}",
out.sql
);
let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected Object shape, got {:?}", out.shape.root)
};
let coauthors = pointers
.iter()
.find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "coauthors"))
.unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
let crate::query::ShapeNode::Array { element, .. } = coauthors else {
unreachable!()
};
assert!(
matches!(
element.as_ref(),
crate::query::ShapeNode::Object { type_name: Some(t), .. } if t == "default::Person"
),
"the elements should hydrate as objects, got {element:?}",
);
}
#[test]
fn test_multi_sort_with_then_emits_two_order_keys() {
let out = compile_and_emit("SELECT Person { name } ORDER BY .name THEN .age DESC");
assert!(out.sql.contains("ORDER BY"), "expected ORDER BY");
assert!(out.sql.contains("\"name\""));
assert!(out.sql.contains("\"age\""));
assert!(out.sql.contains("DESC"));
}
#[test]
fn test_string_index_emits_str_subscript() {
let out = compile_and_emit("SELECT 'hello'[1]");
assert!(
out.sql.contains("_pylon.str_subscript"),
"expected _pylon.str_subscript() for string index, got:\n{}",
out.sql
);
}
#[test]
fn test_for_over_a_set_returning_call_is_not_a_values_clause() {
let out = compile_and_emit("FOR n IN array_unpack(<array<int64>>$ns) UNION (SELECT n)");
assert!(
!out.sql.contains("VALUES (unnest"),
"a set-returning iterator must not land in VALUES, got:\n{}",
out.sql
);
assert!(
out.sql.contains("SELECT unnest"),
"expected the iterator in a select list, got:\n{}",
out.sql
);
}
#[test]
fn test_for_over_array_unpack_binds_the_element_type() {
let out = compile_and_emit("FOR j IN array_unpack(<array<json>>$rows) UNION (SELECT <str>j['k'])");
assert!(
out.sql.contains("::jsonb AS v"),
"the loop variable should carry the element type, got:\n{}",
out.sql
);
}
#[test]
fn test_a_binding_keeps_its_array_type_through_an_aggregate_and_a_condition() {
for query in [
"WITH ids := ([<uuid>$a] IF true ELSE [<uuid>$b]) SELECT Person FILTER contains(ids, .id)",
"WITH ns := array_agg((SELECT Person.name)) SELECT Person FILTER contains(ns, .name)",
] {
let out = compile_and_emit(query);
assert!(
out.sql.contains("@> ARRAY["),
"expected the array overload of contains for `{query}`, got:\n{}",
out.sql
);
}
}
#[test]
fn test_an_array_literals_walk_element_is_read_as_one_value() {
let out = compile_and_emit("WITH p := (SELECT Person LIMIT 1) SELECT [<uuid>$a, p.id]");
assert!(
!out.sql.contains("ARRAY(SELECT"),
"the element should be a scalar subquery, not an array of its own:\n{}",
out.sql
);
}
#[test]
fn test_ordering_a_walk_leaves_the_inner_filter_on_its_own_subject() {
let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title } ORDER BY .title");
assert!(
out.sql.contains("WHERE (\"t0\".\"name\" = 'x')"),
"the filter belongs to the Person the walk starts from, got:\n{}",
out.sql
);
assert!(
out.sql.contains("\"title\" ASC"),
"the ordering belongs to the Post the walk lands on, got:\n{}",
out.sql
);
}
#[test]
fn test_a_walks_head_keeps_its_own_limit() {
let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title }");
let (head, walk) = out.sql.split_once(")\nSELECT").expect("expected a bound head");
assert!(
head.contains("LIMIT 1"),
"the head keeps its own row count, got:\n{}",
out.sql
);
assert!(
!walk.contains("LIMIT"),
"the walk keeps every row it reaches, got:\n{}",
out.sql
);
}
#[test]
fn test_for_over_json_array_unpack_binds_json() {
let out = compile_and_emit(
"FOR entry IN json_array_unpack(to_json(<str>$rows)) UNION (SELECT <str>json_get(entry, 'k'))",
);
assert!(
out.sql.contains("::jsonb AS v"),
"the loop variable should carry json, got:\n{}",
out.sql
);
}
#[test]
fn test_casting_json_get_reads_the_value_not_its_json_spelling() {
let out = compile_and_emit("SELECT <uuid><str>json_get(<json>$p, 'id')");
assert!(
out.sql.contains("#>> '{}'"),
"expected the json value to be read out, got:\n{}",
out.sql
);
}
#[test]
fn test_indexing_json_emits_a_jsonb_accessor() {
let out = compile_and_emit("SELECT (<json>$payload)['label']");
assert!(
out.sql.contains("->'label'"),
"expected a jsonb field access, got:\n{}",
out.sql
);
assert!(
!out.sql.contains("str_subscript"),
"json must not be subscripted as a string, got:\n{}",
out.sql
);
}
#[test]
fn test_casting_json_to_a_scalar_unwraps_it() {
let out = compile_and_emit("SELECT <str>to_json(<str>$s)");
assert!(
out.sql.contains("#>> '{}'"),
"expected the json value to be extracted, got:\n{}",
out.sql
);
}
#[test]
fn test_a_binding_that_reads_the_loop_variable_is_keyed_by_iteration() {
let out = compile_and_emit(
"WITH made := (FOR p IN (SELECT Person) UNION ( \
WITH mine := (SELECT p.posts LIMIT 1) \
INSERT Company { name := mine.title } \
)) SELECT count(made)",
);
assert!(
out.sql.contains("\"mine\" AS (\nSELECT \"_for_p\".\"v\" AS \"_outer\""),
"the binding must be evaluated per iteration, got:\n{}",
out.sql
);
assert!(
out.sql.contains("\"mine\".\"_outer\" = \"_for_p\".\"v\""),
"a read of it must pin to the iteration in scope, got:\n{}",
out.sql
);
}
#[test]
fn test_a_loop_nested_in_a_loop_pairs_its_rows_by_iteration() {
let out = compile_and_emit(
"FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (FOR t IN {n} UNION (INSERT Post { title := t })) })",
);
assert!(
out.sql.contains("\"_outer\""),
"the nested rows must carry an iteration key, got:\n{}",
out.sql
);
assert!(
!out.sql.contains("CROSS JOIN \"_nested_dml_0\""),
"they must not be cross-joined onto every row, got:\n{}",
out.sql
);
}
#[test]
fn test_for_bodied_insert_writes_its_multi_link() {
let out = compile_and_emit("FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (SELECT Post) })");
assert!(
out.sql.contains("INSERT INTO \"public\".\"Person.posts\""),
"the junction rows must be written too, got:\n{}",
out.sql
);
}
#[test]
fn test_string_slice_emits_substr() {
let out = compile_and_emit("SELECT 'hello'[1:3]");
assert!(
out.sql.contains("substr"),
"expected substr() for string slice, got:\n{}",
out.sql
);
}
#[test]
fn test_array_index_emits_subscript() {
let out = compile_and_emit("SELECT [1, 2, 3][1]");
assert!(
out.sql.contains("_pylon.array_subscript"),
"expected _pylon.array_subscript() for array index, got:\n{}",
out.sql
);
}
#[test]
fn test_index_into_array_returning_function_emits_array_subscript() {
let out = compile_and_emit("SELECT str_split('a::b', '::')[-1]");
assert!(
out.sql.contains("_pylon.array_subscript(string_to_array"),
"expected _pylon.array_subscript() over str_split, got:\n{}",
out.sql
);
}
#[test]
fn test_array_slice_emits_subscript() {
let out = compile_and_emit("SELECT [1, 2, 3][0:2]");
assert!(
!out.sql.contains("substr"),
"should not use substr for array, got:\n{}",
out.sql
);
assert!(out.sql.contains(")["), "expected array slice syntax, got:\n{}", out.sql);
}
#[test]
fn test_open_ended_string_slice_emits_substr_no_length() {
let out = compile_and_emit("SELECT 'hello'[2:]");
assert!(out.sql.contains("substr"), "expected substr(), got:\n{}", out.sql);
let substr_idx = out.sql.find("substr").unwrap();
let after = &out.sql[substr_idx..];
let commas = after.chars().take_while(|&c| c != ')').filter(|&c| c == ',').count();
assert_eq!(commas, 1, "open-ended slice should use 2-arg substr, got:\n{}", out.sql);
}
#[test]
fn test_group_by_single_key() {
let out = compile_and_emit("group Person { name } by .age");
assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
assert!(out.sql.contains("\"age\""), "expected age column, got:\n{}", out.sql);
assert!(
out.sql.contains("array_agg(ROW("),
"expected array_agg, got:\n{}",
out.sql
);
assert!(
out.sql.contains("ARRAY['age']"),
"expected grouping array, got:\n{}",
out.sql
);
assert!(matches!(out.shape.root, crate::query::ShapeNode::Group { .. }));
if let crate::query::ShapeNode::Group {
key_nodes,
grouping_position,
elements_position,
..
} = &out.shape.root
{
assert_eq!(key_nodes.len(), 1);
assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "age"));
assert_eq!(*grouping_position, 2);
assert_eq!(*elements_position, 3);
}
}
#[test]
fn test_group_using_alias() {
let out = compile_and_emit("group Person using decade := .age // 10 by decade");
assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
assert!(
out.sql.contains("ARRAY['decade']"),
"expected grouping array, got:\n{}",
out.sql
);
if let crate::query::ShapeNode::Group { key_nodes, .. } = &out.shape.root {
assert_eq!(key_nodes.len(), 1);
assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, .. } if name == "decade"));
}
}
#[test]
fn test_group_orders_elements_within_each_group() {
let out = compile_and_emit("group Person { name } by .age order by .name desc");
assert!(
out.sql.contains(")::record ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
"{}",
out.sql
);
assert!(!out.sql.contains("row_number()"), "{}", out.sql);
}
#[test]
fn test_group_limit_trims_each_group_not_the_result() {
let out = compile_and_emit("group Person { name } by .age order by .name desc limit 1");
assert!(
out.sql
.contains("row_number() OVER (PARTITION BY \"t0\".\"age\" ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
"{}",
out.sql
);
assert!(
out.sql.contains("WHERE \"__rk\" > 0 AND \"__rk\" <= 0 + 1"),
"{}",
out.sql
);
assert!(!out.sql.contains("\nLIMIT"), "{}", out.sql);
}
#[test]
fn test_group_by_two_keys_with_a_per_group_limit() {
let out = compile_and_emit(
"group Person { name } by .age, .name filter .age > 18 order by .name desc offset 1 limit 2",
);
assert!(
out.sql.contains("PARTITION BY \"t0\".\"age\", \"t0\".\"name\""),
"{}",
out.sql
);
let where_pos = out.sql.find("WHERE (\"t0\".\"age\" > 18)").expect("filter");
let rank_pos = out.sql.find("WHERE \"__rk\"").expect("rank filter");
assert!(where_pos < rank_pos, "{}", out.sql);
assert!(
out.sql.contains("GROUP BY \"t0\".\"age\", \"t0\".\"name\""),
"{}",
out.sql
);
}
#[test]
fn test_group_filter_without_a_limit_is_a_plain_where() {
let out = compile_and_emit("group Person { name } by .age filter .age > 18");
assert!(out.sql.contains("WHERE (\"t0\".\"age\" > 18)"), "{}", out.sql);
assert!(!out.sql.contains("row_number()"), "{}", out.sql);
}
#[test]
fn test_abs_path_concat_same_type() {
let out = compile_and_emit("SELECT Person.name ++ ' ' ++ Person.name");
assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
assert!(out.sql.contains("||"), "expected concat operator, got:\n{}", out.sql);
assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
}
#[test]
fn test_abs_path_single_property() {
let out = compile_and_emit("SELECT Person.name");
assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
}
#[test]
fn test_pgvector_cast_emits_vector_type() {
let out = compile_and_emit("SELECT <pgvector::vector>[1.0, 2.0, 3.0]");
assert!(
out.sql.contains("::vector"),
"expected ::vector cast, got:\n{}",
out.sql
);
assert!(out.sql.contains("ARRAY["), "expected ARRAY literal, got:\n{}", out.sql);
}
#[test]
fn test_pgvector_euclidean_distance_emits_l2_operator() {
let out = compile_and_emit(
"SELECT pgvector::euclidean_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
);
assert!(out.sql.contains("<->"), "expected <-> operator, got:\n{}", out.sql);
}
#[test]
fn test_pgvector_cosine_distance_emits_cosine_operator() {
let out = compile_and_emit(
"SELECT pgvector::cosine_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
);
assert!(out.sql.contains("<=>"), "expected <=> operator, got:\n{}", out.sql);
}
#[test]
fn test_pgvector_neg_inner_product_emits_ip_operator() {
let out = compile_and_emit(
"SELECT pgvector::neg_inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
);
assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
}
#[test]
fn test_pgvector_inner_product_negates_ip_operator() {
let out = compile_and_emit(
"SELECT pgvector::inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
);
assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
assert!(out.sql.contains("0.0"), "expected negation of <#>, got:\n{}", out.sql);
}
#[test]
fn test_crypto_digest_str_and_bytes_overloads_both_use_pgcrypto_digest() {
let out = compile_and_emit("SELECT crypto::digest('hello', 'sha256')");
assert!(
out.sql.contains("digest("),
"expected pgcrypto's digest(), got:\n{}",
out.sql
);
let out = compile_and_emit("SELECT crypto::digest(std::from_hex('68656c6c6f'), 'sha256')");
assert!(
out.sql.contains("digest("),
"expected pgcrypto's digest(), got:\n{}",
out.sql
);
}
#[test]
fn test_crypto_hmac_str_and_bytes_overloads_both_use_pgcrypto_hmac() {
let out = compile_and_emit("SELECT crypto::hmac('hello', 'key', 'sha256')");
assert!(
out.sql.contains("hmac("),
"expected pgcrypto's hmac(), got:\n{}",
out.sql
);
let out =
compile_and_emit("SELECT crypto::hmac(std::from_hex('68656c6c6f'), std::from_hex('6b6579'), 'sha256')");
assert!(
out.sql.contains("hmac("),
"expected pgcrypto's hmac(), got:\n{}",
out.sql
);
}
#[test]
fn test_crypto_gen_salt_zero_arg_defaults_to_blowfish() {
let out = compile_and_emit("SELECT crypto::gen_salt()");
assert!(
out.sql.contains("gen_salt('bf')"),
"expected default 'bf' salt type, got:\n{}",
out.sql
);
}
#[test]
fn test_crypto_gen_salt_one_arg_passes_type_through() {
let out = compile_and_emit("SELECT crypto::gen_salt('xdes')");
assert!(
out.sql.contains("gen_salt("),
"expected gen_salt() call, got:\n{}",
out.sql
);
}
#[test]
fn test_crypto_gen_salt_iter_count_casts_to_int4() {
let out = compile_and_emit("SELECT crypto::gen_salt('xdes', 5)");
assert!(
out.sql.contains("::int4"),
"expected int8 -> int4 narrowing cast, got:\n{}",
out.sql
);
}
#[test]
fn test_crypto_crypt_uses_pgcrypto_crypt() {
let out = compile_and_emit("SELECT crypto::crypt('hunter2', crypto::gen_salt())");
assert!(
out.sql.contains("crypt("),
"expected pgcrypto's crypt(), got:\n{}",
out.sql
);
}
#[test]
fn test_postgis_cast_emits_geometry_type() {
let out = compile_and_emit("SELECT <postgis::geometry>'POINT(1 2)'");
assert!(
out.sql.contains("::geometry"),
"expected ::geometry cast, got:\n{}",
out.sql
);
}
#[test]
fn test_postgis_x_uses_st_x_builtin() {
let out = compile_and_emit("SELECT postgis::x(<postgis::geometry>'POINT(1 2)')");
assert!(out.sql.contains("st_x("), "expected st_x() call, got:\n{}", out.sql);
}
#[test]
fn test_postgis_area_geometry_and_geography_overloads() {
let out = compile_and_emit("SELECT postgis::area(<postgis::geometry>'POINT(1 2)')");
assert!(
out.sql.contains("st_area("),
"expected st_area() call, got:\n{}",
out.sql
);
let out = compile_and_emit("SELECT postgis::area(<postgis::geography>'POINT(1 2)', true)");
assert!(
out.sql.contains("st_area("),
"expected st_area() call, got:\n{}",
out.sql
);
}
#[test]
fn test_postgis_setsrid_casts_int64_arg_to_int4() {
let out = compile_and_emit("SELECT postgis::setsrid(<postgis::geometry>'POINT(1 2)', 4326)");
assert!(
out.sql.contains("st_setsrid("),
"expected st_setsrid() call, got:\n{}",
out.sql
);
assert!(
out.sql.contains("::int4"),
"expected int8 -> int4 narrowing cast, got:\n{}",
out.sql
);
}
#[test]
fn test_postgis_quantizecoordinates_default_arity_variants_compile() {
let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5)");
assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5, 5, 5)");
assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
}
#[test]
fn test_postgis_op_contains_emits_infix_operator_not_function_call() {
let out = compile_and_emit(
"SELECT postgis::op_contains(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
);
assert!(out.sql.contains(" ~ "), "expected infix ~ operator, got:\n{}", out.sql);
assert!(
!out.sql.contains("op_contains("),
"must not call a literal op_contains function, got:\n{}",
out.sql
);
}
#[test]
fn test_postgis_op_overlaps_geometry_and_geography_overloads() {
let out = compile_and_emit(
"SELECT postgis::op_overlaps(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
);
assert!(
out.sql.contains(" && "),
"expected infix && operator, got:\n{}",
out.sql
);
let out = compile_and_emit(
"SELECT postgis::op_overlaps(<postgis::geography>'POINT(1 2)', <postgis::geography>'POINT(3 4)')",
);
assert!(
out.sql.contains(" && "),
"expected infix && operator, got:\n{}",
out.sql
);
}
fn make_schema_with_fns() -> SchemaDescriptor {
let mut s = make_schema();
s.functions = vec![
FunctionDescriptor {
name: "mysum".into(),
module: "default".into(),
params: vec![
FunctionParamDescriptor {
name: "a".into(),
pg_type: "int8".into(),
},
FunctionParamDescriptor {
name: "b".into(),
pg_type: "int8".into(),
},
],
return_pg_type: "int8".into(),
return_is_object: false,
return_is_set: false,
return_is_polymorphic: false,
volatility: "immutable".into(),
body: "a + b".into(),
},
FunctionDescriptor {
name: "adults".into(),
module: "default".into(),
params: vec![],
return_pg_type: "default::Person".into(),
return_is_object: true,
return_is_set: true,
return_is_polymorphic: false,
volatility: "stable".into(),
body: "select Person filter .age > 18".into(),
},
FunctionDescriptor {
name: "mysum".into(),
module: "default".into(),
params: vec![
FunctionParamDescriptor {
name: "a".into(),
pg_type: "int8".into(),
},
FunctionParamDescriptor {
name: "b".into(),
pg_type: "int8".into(),
},
FunctionParamDescriptor {
name: "c".into(),
pg_type: "int8".into(),
},
],
return_pg_type: "int8".into(),
return_is_object: false,
return_is_set: false,
return_is_polymorphic: false,
volatility: "immutable".into(),
body: "a + b + c".into(),
},
];
s
}
#[test]
fn test_user_fn_scalar_call() {
let schema = make_schema_with_fns();
let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
assert!(out.sql.contains("\"public\".\"mysum\""), "got:\n{}", out.sql);
}
#[test]
fn test_user_fn_overload_resolved_by_argument_count() {
let schema = make_schema_with_fns();
let out = compile_and_emit_with("SELECT mysum(1, 2, 3)", &schema);
assert!(
out.sql
.contains("\"public\".\"mysum\"((1)::int8, (2)::int8, (3)::int8)"),
"got:\n{}",
out.sql,
);
let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
assert!(
out.sql.contains("\"public\".\"mysum\"((1)::int8, (2)::int8)"),
"got:\n{}",
out.sql,
);
}
#[test]
fn test_user_fn_object_select_no_shape() {
let schema = make_schema_with_fns();
let out = compile_and_emit_with("SELECT adults()", &schema);
assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
assert!(out.sql.contains("FROM"), "got:\n{}", out.sql);
}
#[test]
fn test_user_fn_object_select_with_shape() {
let schema = make_schema_with_fns();
let out = compile_and_emit_with("SELECT adults() { name }", &schema);
assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
assert!(out.sql.contains("\"name\""), "got:\n{}", out.sql);
}
#[test]
fn test_object_fn_projected_to_a_property_inside_an_expression() {
let schema = make_schema_with_fns();
let out = compile_and_emit_with("SELECT Person { n := adults().name }", &schema);
assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
assert!(out.sql.contains("\".\"name\"\n"), "got:\n{}", out.sql);
}
#[test]
fn test_object_fn_sub_select_carries_its_modifiers() {
let schema = make_schema_with_fns();
let out = compile_and_emit_with(
"SELECT Person { n := (select adults() filter .age > 21 limit 1).name }",
&schema,
);
assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
assert!(out.sql.contains("\"age\" > 21"), "got:\n{}", out.sql);
assert!(out.sql.contains("LIMIT 1"), "got:\n{}", out.sql);
}
#[test]
fn test_object_fn_without_a_property_still_explains_the_restriction() {
let schema = make_schema_with_fns();
let ast = parse::parse("SELECT Person { n := adults() + 1 }").unwrap();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
};
assert!(err.contains("returns objects"), "{err}");
}
#[test]
fn test_object_fn_projected_to_an_unknown_property_is_rejected() {
let schema = make_schema_with_fns();
let ast = parse::parse("SELECT Person { n := adults().nope }").unwrap();
let err = match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected a compile error"),
Err(e) => format!("{e}"),
};
assert!(err.contains("nope"), "{err}");
}
#[test]
fn test_user_fn_in_cte_exposes_raw_columns() {
let schema = make_schema_with_fns();
let out = compile_and_emit_with("WITH persons := adults() SELECT persons FILTER .age > 25", &schema);
assert!(
out.sql.contains("SELECT * FROM \"public\".\"adults\"()"),
"CTE source must be SELECT * FROM fn(), got:\n{}",
out.sql,
);
assert!(
out.sql.contains("\"age\""),
"outer filter must reference raw column, got:\n{}",
out.sql
);
}
fn make_schema_with_vector() -> SchemaDescriptor {
use crate::schema::VectorIndexDescriptor;
let mut s = make_schema();
if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
td.vector_indexes.push(VectorIndexDescriptor {
index_name: None,
pointers: vec!["name".into()],
model: "test-embed".into(),
metric: "cosine".into(),
dimensions: 4,
});
}
s
}
#[test]
fn test_vector_search_bare_type_name() {
let schema = make_schema_with_vector();
let out = compile_and_emit_with(
"WITH search := vector::search(Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
SELECT search { object { name }, distance }",
&schema,
);
assert!(
out.sql.contains("\"Person\""),
"expected Person table, got:\n{}",
out.sql
);
assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
}
#[test]
fn test_vector_search_qualified_type_name() {
let schema = make_schema_with_vector();
let out = compile_and_emit_with(
"WITH search := vector::search(default::Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
SELECT search { object { name }, distance }",
&schema,
);
assert!(
out.sql.contains("\"Person\""),
"expected Person table, got:\n{}",
out.sql
);
assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
}
#[test]
fn test_vector_search_subquery_filter_included_in_where() {
let schema = make_schema_with_vector();
let out = compile_and_emit_with(
"WITH search := vector::search((select Person filter .name = 'Alice'), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
SELECT search { object { name }, distance }",
&schema,
);
assert!(
out.sql.contains("\"Person\""),
"expected Person table, got:\n{}",
out.sql
);
assert!(out.sql.contains("\"name\""), "expected name filter, got:\n{}", out.sql);
assert!(out.sql.contains("Alice"), "expected filter value, got:\n{}", out.sql);
assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
}
#[test]
fn test_vector_search_subquery_filter_combined_with_outer_property_filter() {
let schema = make_schema_with_vector();
let out = compile_and_emit_with(
"WITH search := vector::search((select Person filter .age > 18), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
SELECT search { object { name }, distance }",
&schema,
);
assert!(
out.sql.contains("\"age\""),
"expected age pre-filter, got:\n{}",
out.sql
);
assert!(out.sql.contains("18"), "expected filter value 18, got:\n{}", out.sql);
assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
}
#[test]
fn test_vector_search_text_overload_with_subquery_filter() {
let schema = make_schema_with_vector();
let out = compile_and_emit_with(
"WITH search := vector::search((select Person filter .name = 'Alice'), query := $q) \
SELECT search { object { name }, distance }",
&schema,
);
assert!(
out.sql.contains("\"Person\""),
"expected Person table, got:\n{}",
out.sql
);
assert!(
out.sql.contains("Alice"),
"expected pre-filter value, got:\n{}",
out.sql
);
assert!(
out.sql.contains("float8[]"),
"expected float8[] cast for deferred vec param, got:\n{}",
out.sql
);
assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
}
#[test]
fn test_count_type_ref_compiles_to_agg_over_query() {
let out = compile_and_emit("SELECT count(Person)");
assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
assert!(
out.sql.contains("\"Person\""),
"expected Person table, got:\n{}",
out.sql
);
}
#[test]
fn test_count_qualified_type_ref_compiles_to_agg_over_query() {
let out = compile_and_emit("SELECT count(default::Person)");
assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
assert!(
out.sql.contains("\"Person\""),
"expected Person table, got:\n{}",
out.sql
);
}
#[test]
fn test_count_subquery_compiles_to_agg_over_query() {
let out = compile_and_emit("SELECT count((select Person))");
assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
assert!(
out.sql.contains("\"Person\""),
"expected Person table, got:\n{}",
out.sql
);
}
#[test]
fn test_count_subquery_with_filter() {
let out = compile_and_emit("SELECT count((select Person filter .name = 'Alice'))");
assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
assert!(
out.sql.contains("\"name\""),
"expected filter on name, got:\n{}",
out.sql
);
}
#[test]
fn test_positional_param_compiles_to_dollar_n() {
let out = compile_and_emit("SELECT Person FILTER .name = $0");
assert!(out.sql.contains("$1"), "expected $1 placeholder, got:\n{}", out.sql);
}
#[test]
fn test_multiple_positional_params_compile_in_order() {
let out = compile_and_emit("SELECT Person FILTER .name = $0 AND .age > $1");
assert!(out.sql.contains("$1"), "expected $1, got:\n{}", out.sql);
assert!(out.sql.contains("$2"), "expected $2, got:\n{}", out.sql);
}
#[test]
fn test_repeated_positional_param_reuses_slot() {
let out = compile_and_emit("SELECT Person FILTER .name = $0 OR .name = $0");
assert_eq!(
out.sql.matches("$1").count(),
2,
"both uses must reference $1, got:\n{}",
out.sql
);
}
#[test]
fn test_cast_to_nonexistent_type_names_full_type() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .name = <default::Ghost>$name").unwrap();
match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected compile error for unknown type"),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("unknown type 'default::Ghost'"),
"expected full type name in error, got: {msg}",
);
}
}
}
#[test]
fn test_cast_to_nonexistent_unqualified_type_names_type() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .name = <Ghost>$name").unwrap();
match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected compile error for unknown type"),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("unknown type 'Ghost'"),
"expected type name in error, got: {msg}",
);
}
}
}
#[test]
fn test_top_level_cast_to_registered_scalar_uses_its_own_domain() {
use crate::schema::ScalarDescriptor;
let mut schema = make_schema();
schema.scalars.push(ScalarDescriptor {
name: "Email".into(),
module: "default".into(),
base: "Str".into(),
pg_type: "text".into(),
check_constraints: vec!["value ~ '^[^@]+@[^@]+\\.[^@]+$'".into()],
is_sequence: false,
});
let out = compile_and_emit_with("SELECT <default::Email>'test@test.de'", &schema);
assert!(
out.sql.contains("\"public\".\"Email\""),
"expected a cast to the scalar's own domain, got:\n{}",
out.sql
);
assert!(
!out.sql.to_lowercase().contains("\"person\""),
"must not be misrouted to an object-type lookup, got:\n{}",
out.sql
);
}
#[test]
fn test_top_level_cast_to_unknown_type_names_full_type() {
let schema = make_schema();
let ast = parse::parse("SELECT <default::Ghost>$name").unwrap();
match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected compile error for unknown type"),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("unknown type 'default::Ghost'"),
"expected full type name in error, got: {msg}",
);
}
}
}
#[test]
fn test_structural_tuple_cast_unnamed_resolves_to_jsonb() {
let out = compile_and_emit("SELECT <tuple<str, bool>>$p");
assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
}
#[test]
fn test_jsonb_to_uuid_cast_extracts_via_text() {
let out = compile_and_emit("SELECT <uuid>(<json>$p)");
assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
}
#[test]
fn test_jsonb_to_datetime_cast_extracts_via_text() {
let out = compile_and_emit("SELECT <datetime>(<json>$p)");
assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
assert!(out.sql.contains("::timestamptz"), "got:\n{}", out.sql);
}
#[test]
fn test_jsonb_to_duration_cast_extracts_via_text() {
let out = compile_and_emit("SELECT <duration>(<json>$p)");
assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
assert!(out.sql.contains("::interval"), "got:\n{}", out.sql);
}
#[test]
fn test_jsonb_to_array_cast_unpacks_each_element() {
let out = compile_and_emit("SELECT <array<int64>>(<json>$p)");
assert!(out.sql.contains("jsonb_array_elements("), "got:\n{}", out.sql);
assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
assert!(out.sql.contains("::int8"), "got:\n{}", out.sql);
}
#[test]
fn test_non_jsonb_cast_is_unaffected_by_jsonb_extraction() {
let out = compile_and_emit("SELECT <uuid>$p");
assert!(!out.sql.contains("#>>"), "got:\n{}", out.sql);
assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
}
#[test]
fn test_datetime_plus_duration_type_checks() {
let out = compile_and_emit("SELECT <datetime>$p + <duration>$d");
assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
}
#[test]
fn test_duration_plus_datetime_type_checks_either_order() {
let out = compile_and_emit("SELECT <duration>$d + <datetime>$p");
assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
}
#[test]
fn test_local_date_plus_duration_type_checks() {
let out = compile_and_emit("SELECT <cal::local_date>$p + <duration>$d");
assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
}
#[test]
fn test_datetime_minus_duration_type_checks() {
let out = compile_and_emit("SELECT <datetime>$p - <duration>$d");
assert!(out.sql.contains(" - "), "got:\n{}", out.sql);
}
#[test]
fn test_local_time_plus_duration_type_checks() {
let out = compile_and_emit("SELECT <cal::local_time>$p + <duration>$d");
assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
}
#[test]
fn test_datetime_plus_duration_only_allowed_for_add_and_sub() {
let schema = make_schema();
let ast = parse::parse("SELECT <datetime>$p = <duration>$d").unwrap();
match ir::compile(&ast, &schema) {
Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
Ok(_) => panic!("expected a compile error"),
}
}
#[test]
fn test_int_and_float_mix_is_allowed() {
let out = compile_and_emit("SELECT <int16>1 + <float32>2.0");
assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
}
#[test]
fn test_int_and_decimal_mix_is_allowed() {
let out = compile_and_emit("SELECT <int64>1 + <decimal>2.0");
assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
}
#[test]
fn test_float_and_decimal_mix_still_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT <decimal>$p + <float64>$n").unwrap();
match ir::compile(&ast, &schema) {
Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
Ok(_) => panic!("expected a compile error"),
}
}
#[test]
fn test_unknown_pointer_suggests_a_close_match() {
let schema = make_schema();
let ast = parse::parse("SELECT Person { nam }").unwrap();
match ir::compile(&ast, &schema) {
Err(err) => assert_eq!(
format!("{err}"),
"object type 'default::Person' has no link or property 'nam'. Did you mean 'name'?"
),
Ok(_) => panic!("expected a compile error"),
}
}
#[test]
fn test_unrelated_unknown_pointer_gets_no_suggestion() {
let schema = make_schema();
let ast = parse::parse("SELECT Person { xyzxyzxyz }").unwrap();
match ir::compile(&ast, &schema) {
Err(err) => {
let msg = format!("{err}");
assert!(msg.contains("has no link or property 'xyzxyzxyz'"), "got: {msg}");
assert!(!msg.contains("Did you mean"), "got: {msg}");
}
Ok(_) => panic!("expected a compile error"),
}
}
#[test]
fn test_multilink_sourced_backlink_exists_filter_compiles() {
let out = compile_and_emit("SELECT Post filter exists .<posts[is Person]");
assert!(out.sql.contains("EXISTS"), "got:\n{}", out.sql);
}
#[test]
fn test_backlink_as_computed_shape_pointer_compiles() {
let out = compile_and_emit("SELECT Post { title, authors := .<posts[is Person] { name } }");
assert!(out.sql.contains("array_agg(ROW("), "got:\n{}", out.sql);
}
fn schema_with_exclusive_posts() -> SchemaDescriptor {
let mut schema = make_schema();
for td in schema.types.iter_mut().filter(|td| td.name == "Person") {
for ml in td.multilinks.iter_mut().filter(|ml| ml.name == "posts") {
ml.is_exclusive = true;
}
}
schema
}
#[test]
fn test_backlink_through_an_exclusive_multilink_is_single() {
let out = compile_and_emit_with(
"SELECT Post { title, author := .<posts[is Person] { name } }",
&schema_with_exclusive_posts(),
);
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected an object root, got {:?}", out.shape.root)
};
assert!(
matches!(pointers.last(), Some(ShapeNode::Object { name, .. }) if name == "author"),
"got {pointers:?}"
);
}
#[test]
fn test_a_path_through_an_exclusive_backlink_is_single() {
let out = compile_and_emit_with(
"SELECT Post { title, author_name := .<posts[is Person].name }",
&schema_with_exclusive_posts(),
);
let ShapeNode::Object { pointers, .. } = &out.shape.root else {
panic!("expected an object root, got {:?}", out.shape.root)
};
assert!(
matches!(pointers.last(), Some(ShapeNode::Scalar { name, .. }) if name == "author_name"),
"got {pointers:?}"
);
assert!(!out.sql.contains("ARRAY("), "got:\n{}", out.sql);
}
#[test]
fn test_structural_tuple_cast_named_resolves_to_jsonb() {
let out = compile_and_emit("SELECT <tuple<x: float64, y: float64>>$p");
assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
}
#[test]
fn test_structural_tuple_cast_nested_resolves_to_jsonb() {
let out = compile_and_emit("SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>$p");
assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
}
#[test]
fn test_nominal_named_tuple_cast_resolves_to_jsonb() {
let mut schema = make_schema();
schema.named_tuples.push(NamedTupleDescriptor {
name: "Point".into(),
module: "default".into(),
members: vec![],
});
let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
}
#[test]
fn test_array_literal_cast_resolves_to_native_pg_array_not_jsonb() {
let out = compile_and_emit("SELECT <array<str>>['foo', 'bar']");
assert!(
out.sql.contains("::text[]") || out.sql.contains("ARRAY["),
"got:\n{}",
out.sql
);
assert!(
!out.sql.contains("jsonb"),
"arrays must not use jsonb, got:\n{}",
out.sql
);
}
#[test]
fn test_array_literal_cast_applies_per_element_cast() {
let out = compile_and_emit("SELECT <array<int64>>['1', '2']");
assert!(out.sql.contains("ARRAY[('1')::int8, ('2')::int8]"), "got:\n{}", out.sql);
}
#[test]
fn test_array_param_cast_uses_direct_suffix_cast() {
let out = compile_and_emit("SELECT <array<int64>>$p");
assert!(out.sql.contains("::int8[]"), "got:\n{}", out.sql);
}
#[test]
fn test_array_of_named_tuple_element_casts_to_jsonb_array() {
let out = compile_and_emit("SELECT <array<tuple<x: float64, y: float64>>>$p");
assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
}
#[test]
fn test_contains_on_array_literal_cast_uses_array_overload_not_strpos() {
let out = compile_and_emit("SELECT contains(<array<str>>[1, 2], '2')");
assert!(out.sql.contains("@> ARRAY["), "got:\n{}", out.sql);
assert!(
!out.sql.contains("strpos"),
"must not fall back to the str/str overload, got:\n{}",
out.sql
);
}
#[test]
fn test_nested_array_type_rejected_at_parse_time() {
match parse::parse("SELECT <array<array<str>>>$p") {
Ok(_) => panic!("expected parse error for nested array type"),
Err(e) => assert!(e.to_string().contains("nested arrays are not supported"), "got: {}", e),
}
}
#[test]
fn test_array_cast_in_computed_shape_field_schema_bound_context() {
let out = compile_and_emit("SELECT Person { name, tags := <array<str>>['a', 'b'] }");
assert!(out.sql.contains("ARRAY[('a')::text, ('b')::text]"), "got:\n{}", out.sql);
}
#[test]
fn test_nominal_named_tuple_cast_shape_carries_real_members() {
use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
let mut schema = make_schema();
schema.named_tuples.push(NamedTupleDescriptor {
name: "Point".into(),
module: "default".into(),
members: vec![
TupleMemberDescriptor {
name: Some("x".into()),
kind: TupleMemberKind::Scalar {
pg_type: "float8".into(),
},
},
TupleMemberDescriptor {
name: Some("y".into()),
kind: TupleMemberKind::Scalar {
pg_type: "float8".into(),
},
},
],
});
let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
match &out.shape.root {
crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
assert_eq!(type_name.as_deref(), Some("default::Point"));
let members = members.as_ref().expect("expected resolved members");
assert_eq!(members.len(), 2);
assert_eq!(members[0].key.as_deref(), Some("x"));
assert_eq!(members[1].key.as_deref(), Some("y"));
}
other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
}
}
#[test]
fn test_structural_tuple_property_read_shape_carries_real_members() {
use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
let schema = SchemaDescriptor {
types: vec![TypeDescriptor {
name: "Person".into(),
module: "default".into(),
table: "Person".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![PropertyDescriptor {
name: "address".into(),
pg_type: "jsonb".into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: Some(vec![
TupleMemberDescriptor {
name: Some("street".into()),
kind: TupleMemberKind::Scalar { pg_type: "text".into() },
},
TupleMemberDescriptor {
name: Some("zip".into()),
kind: TupleMemberKind::Scalar { pg_type: "text".into() },
},
]),
column_type: None,
}],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
}],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
};
let out = compile_and_emit_with("SELECT Person { address }", &schema);
assert!(out.sql.contains("::jsonb"), "got:\n{}", out.sql);
match &out.shape.root {
crate::query::ShapeNode::Object { pointers, .. } => {
let address = pointers
.iter()
.find(|p| matches!(p, crate::query::ShapeNode::NamedTuple { name, .. } if name == "address"))
.expect("expected address pointer in shape");
match address {
crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
assert_eq!(*type_name, None);
let members = members.as_ref().expect("expected resolved members");
assert_eq!(members.len(), 2);
assert_eq!(members[0].key.as_deref(), Some("street"));
assert_eq!(members[1].key.as_deref(), Some("zip"));
}
other => panic!("expected NamedTuple, got {other:?}"),
}
}
other => panic!("expected ShapeNode::Object, got {other:?}"),
}
}
#[test]
fn test_bare_path_select_structural_tuple_property_shape_carries_real_members() {
use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
let schema = SchemaDescriptor {
types: vec![TypeDescriptor {
name: "Person".into(),
module: "default".into(),
table: "Person".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![PropertyDescriptor {
name: "address".into(),
pg_type: "jsonb".into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: Some(vec![
TupleMemberDescriptor {
name: Some("street".into()),
kind: TupleMemberKind::Scalar { pg_type: "text".into() },
},
TupleMemberDescriptor {
name: Some("zip".into()),
kind: TupleMemberKind::Scalar { pg_type: "text".into() },
},
]),
column_type: None,
}],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
}],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
};
let out = compile_and_emit_with("SELECT Person.address", &schema);
match &out.shape.root {
crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
assert_eq!(*type_name, None);
let members = members.as_ref().expect("expected resolved members");
assert_eq!(members.len(), 2);
assert_eq!(members[0].key.as_deref(), Some("street"));
assert_eq!(members[1].key.as_deref(), Some("zip"));
}
other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
}
}
#[test]
fn test_path_traversal_into_structural_tuple_property() {
use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
let schema = SchemaDescriptor {
types: vec![TypeDescriptor {
name: "Person".into(),
module: "default".into(),
table: "Person".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![PropertyDescriptor {
name: "address".into(),
pg_type: "jsonb".into(),
nullable: true,
default_sql: None,
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: false,
is_pk: false,
is_readonly: false,
rewrites: vec![],
tuple_members: Some(vec![
TupleMemberDescriptor {
name: Some("street".into()),
kind: TupleMemberKind::Scalar { pg_type: "text".into() },
},
TupleMemberDescriptor {
name: Some("zip".into()),
kind: TupleMemberKind::Scalar { pg_type: "text".into() },
},
]),
column_type: None,
}],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
}],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
};
let out = compile_and_emit_with("SELECT default::Person.address.street", &schema);
assert!(out.sql.contains("\"address\"->'street'"), "got:\n{}", out.sql);
}
#[test]
fn test_structural_tuple_cast_shape_carries_real_members() {
let out = compile_and_emit("SELECT <tuple<street: str, zip: str>>$p");
match &out.shape.root {
crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
assert_eq!(*type_name, None);
let members = members.as_ref().expect("expected resolved members");
assert_eq!(members.len(), 2);
assert_eq!(members[0].key.as_deref(), Some("street"));
assert_eq!(members[1].key.as_deref(), Some("zip"));
}
other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
}
}
#[test]
fn test_tuple_cast_mixed_named_and_unnamed_elements_rejected() {
match parse::parse("SELECT <tuple<x: float64, bool>>$p") {
Ok(_) => panic!("expected parse error for mixed named/unnamed tuple elements"),
Err(e) => assert!(e.to_string().contains("all named or all unnamed"), "got: {}", e),
}
}
#[test]
fn test_is_with_tuple_type_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER Person is tuple<x: float64, y: float64>").unwrap();
match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected error for IS with a tuple type"),
Err(e) => {
assert!(
e.to_string().contains("cannot use IS with a tuple or array type"),
"got: {}",
e
);
}
}
}
#[test]
fn test_is_with_array_type_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER Person is array<str>").unwrap();
match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected error for IS with an array type"),
Err(e) => {
assert!(
e.to_string().contains("cannot use IS with a tuple or array type"),
"got: {}",
e
);
}
}
}
#[test]
fn test_tuple_index_on_non_literal_falls_back_to_jsonb_index() {
let out = compile_and_emit("SELECT (<tuple<int64, str>>('1', 3)).1");
assert!(out.sql.contains("->1"), "got:\n{}", out.sql);
}
#[test]
fn test_tuple_index_out_of_bounds_on_cast_target_errors_at_compile_time() {
let ast = parse::parse("SELECT (<tuple<int64, str>>('1', 3)).2").unwrap();
let schema = make_schema();
match ir::compile(&ast, &schema) {
Ok(_) => panic!("expected out-of-bounds tuple index error"),
Err(e) => {
assert!(
e.to_string()
.contains("2 is not a member of tuple<std::int64, std::str>"),
"got: {}",
e
);
}
}
}
#[test]
fn test_positional_tuple_literal_cast_to_tuple_type_compiles() {
let out = compile_and_emit("SELECT <tuple<int64, str>>(1, 'x')");
assert!(
out.sql.contains("jsonb_build_array((1)::int8, ('x')::text)"),
"got:\n{}",
out.sql
);
}
#[test]
fn test_positional_tuple_literal_cast_coerces_mismatched_literal_types() {
let out = compile_and_emit("SELECT <tuple<int64, str>>('1', 3)");
assert!(
out.sql.contains("jsonb_build_array(('1')::int8, (3)::text)"),
"got:\n{}",
out.sql
);
}
#[test]
fn test_nested_tuple_literal_cast_applies_casts_recursively() {
let out = compile_and_emit(
"SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>(point := ('1', 2), label := 5)",
);
assert!(
out.sql.contains(
"jsonb_build_object('point', jsonb_build_object('x', ('1')::float8, 'y', (2)::float8), 'label', (5)::text)"
),
"got:\n{}",
out.sql
);
}
#[test]
fn test_positional_tuple_literal_nested_inside_named_tuple_compiles() {
let out = compile_and_emit("SELECT (point := (1, 2), label := 'origin')");
assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
assert!(out.sql.contains("jsonb_build_object("), "got:\n{}", out.sql);
}
#[test]
fn test_positional_tuple_literal_in_schema_bound_shape_field_compiles() {
let out = compile_and_emit("SELECT Person { name, pair := (1, 2) }");
assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
}
fn make_schema_with_search_index(backend: crate::schema::SearchBackend) -> SchemaDescriptor {
use crate::schema::{SearchIndexDescriptor, SearchPointerDescriptor, SearchWeight};
let mut s = make_schema();
if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
td.search_indexes.push(SearchIndexDescriptor {
index_name: None,
backend,
pointers: vec![SearchPointerDescriptor {
name: "name".into(),
weight: SearchWeight::A,
}],
});
}
s
}
#[test]
fn test_insert_enqueues_a_meilisearch_outbox_row() {
let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
assert!(
out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
"expected a Meilisearch outbox enqueue CTE, got:\n{}",
out.sql,
);
assert!(
out.sql.contains("INSERT INTO _pylon.\"IndexOutbox\""),
"got:\n{}",
out.sql
);
}
#[test]
fn test_insert_enqueues_an_opensearch_outbox_row() {
let schema = make_schema_with_search_index(crate::schema::SearchBackend::OpenSearch);
let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
assert!(
out.sql.contains("'OpenSearch'::_pylon.\"IndexKind\""),
"expected an OpenSearch outbox enqueue CTE, got:\n{}",
out.sql,
);
}
#[test]
fn test_insert_does_not_enqueue_an_outbox_row_for_a_postgres_backed_search_index() {
let schema = make_schema_with_search_index(crate::schema::SearchBackend::Postgres);
let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
assert!(
!out.sql.contains("_pylon.\"IndexOutbox\""),
"did not expect an outbox enqueue, got:\n{}",
out.sql
);
}
#[test]
fn test_update_enqueues_a_meilisearch_outbox_row() {
let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
let out = compile_and_emit_with("UPDATE Person FILTER .name = 'Alice' SET { age := 31 }", &schema);
assert!(
out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
"expected a Meilisearch outbox enqueue CTE, got:\n{}",
out.sql,
);
}
#[test]
fn test_delete_enqueues_a_meilisearch_outbox_delete_job() {
let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
let out = compile_and_emit_with("DELETE Person FILTER .name = 'Alice'", &schema);
assert!(
out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
"expected a Meilisearch outbox enqueue CTE, got:\n{}",
out.sql,
);
assert!(
out.sql.contains("'delete'"),
"expected the delete operation literal, got:\n{}",
out.sql
);
}
#[test]
fn test_range_intrinsic_resolves_int_literals_to_int8range() {
let out = compile_and_emit("SELECT std::overlaps(std::range(1, 3), std::range(2, 5))");
assert!(out.sql.contains("int8range(1, 3)"), "got:\n{}", out.sql);
assert!(out.sql.contains("int8range(2, 5)"), "got:\n{}", out.sql);
assert!(
out.sql.contains(" && "),
"expected infix && for overlaps, got:\n{}",
out.sql
);
assert!(
!out.sql.contains("\"std\""),
"must not emit a literal std schema call, got:\n{}",
out.sql
);
}
#[test]
fn test_range_intrinsic_resolves_datetime_to_tstzrange() {
let out =
compile_and_emit("SELECT std::range(<datetime>'2024-01-01T00:00:00Z', <datetime>'2024-06-01T00:00:00Z')");
assert!(out.sql.contains("tstzrange("), "got:\n{}", out.sql);
}
#[test]
fn test_range_intrinsic_named_bounds_become_a_bounds_string() {
let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := true, inc_upper := true)");
assert!(out.sql.contains("int8range(1, 3, '[]')"), "got:\n{}", out.sql);
let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := false)");
assert!(out.sql.contains("int8range(1, 3, '()')"), "got:\n{}", out.sql);
}
#[test]
fn test_range_intrinsic_computes_a_bounds_string_from_an_expression() {
let out = compile_and_emit("SELECT std::range(1, 3, inc_upper := <bool>$closed)");
assert!(out.sql.contains("CASE WHEN"), "got:\n{}", out.sql);
}
#[test]
fn test_range_intrinsic_leaves_a_missing_upper_bound_unbounded() {
let out = compile_and_emit("SELECT std::range(1)");
assert!(out.sql.contains("int8range(1, NULL)"), "got:\n{}", out.sql);
}
#[test]
fn test_a_named_only_parameter_is_reported_as_named_only() {
assert!(
compile_err("SELECT std::range(1, 5, true, false)").contains("takes 'inc_lower' as a named argument only"),
"{}",
compile_err("SELECT std::range(1, 5, true, false)")
);
assert!(
compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
.contains("requires the named argument 'value'"),
"{}",
compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
);
assert!(
compile_err("SELECT std::range()").contains("takes 1 or 2 argument(s), got 0"),
"{}",
compile_err("SELECT std::range()")
);
let message = compile_err("SELECT std::json_get(std::to_json('{}'), 'a', 1, 2)");
assert!(message.contains("default := optional<json>"), "{message}");
}
#[test]
fn test_range_intrinsic_builds_the_empty_range() {
let out = compile_and_emit("SELECT std::range(1, 3, empty := true)");
assert!(out.sql.contains("'empty'::int8range"), "got:\n{}", out.sql);
}
#[test]
fn test_multirange_intrinsic_resolves_from_range_element() {
let out = compile_and_emit("SELECT std::multirange([std::range(1, 3), std::range(5, 7)])");
assert!(out.sql.contains("int8multirange(VARIADIC "), "got:\n{}", out.sql);
assert!(
!out.sql.contains("\"std\""),
"must not emit a literal std schema call, got:\n{}",
out.sql
);
}
}