mod compiler;
pub mod tags;
pub use compiler::column_default_sql;
pub use compiler::compile;
pub use compiler::compile_computed_in_type;
pub use compiler::compile_constraint_expr;
pub use compiler::compile_expr_in_type;
pub use compiler::compile_expr_unaliased;
pub use compiler::compile_inlined_default;
pub use compiler::compile_scalar_default;
pub use compiler::compile_scalar_default_typed;
pub use compiler::compile_trigger_handler;
pub use compiler::compile_with_config;
pub use compiler::default_blocker;
pub(crate) use compiler::infer_ir_type;
pub use compiler::inlined_pointer_defaults;
pub use compiler::pg_type_to_pyql;
pub(crate) use compiler::types_compatible;
pub use compiler::{GLOBALS_ARG, compile_fn_body, functions_needing_globals};
pub use compiler::{RewriteAssignment, compile_rewrite_assignments};
use crate::parse::ast::{BinOpKind, UnaryOpKind};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct SessionConfig {
pub allow_user_specified_id: bool,
}
#[derive(Debug, Clone)]
pub enum IrStmt {
Select(IrSelect),
PathSelect(IrPathSelect),
Insert(IrInsert),
Update(IrUpdate),
Delete(IrDelete),
For(IrFor),
Group(IrGroup),
FunctionSelect(IrFunctionSelect),
VectorSearch(IrVectorSearch),
FtsSearch(IrFtsSearch),
ScalarUnion(Vec<IrStmt>),
}
#[derive(Debug, Clone)]
pub struct IrFor {
pub var_name: String,
pub iterator: IrForIterator,
pub body: Box<IrStmt>,
pub body_ctes: Vec<IrCteDef>,
}
#[derive(Debug, Clone)]
pub enum IrForIterator {
Values { exprs: Vec<IrExpr>, pg_type: String },
Query {
stmt: Box<IrStmt>,
scalar: bool,
},
SetReturning { expr: IrExpr, pg_type: String },
}
#[derive(Debug, Clone)]
pub struct IrGroup {
pub source: IrSource,
pub shape: Vec<IrShapePointer>,
pub keys: Vec<(String, IrExpr)>,
pub filter: Option<IrExpr>,
pub order_by: Vec<IrSort>,
pub offset: Option<IrExpr>,
pub limit: Option<IrExpr>,
pub output: IrGroupOutput,
}
#[derive(Debug, Clone)]
pub struct IrGroupProjection {
pub pointers: Vec<IrShapePointer>,
pub order_by: Vec<IrSort>,
pub offset: Option<IrExpr>,
pub limit: Option<IrExpr>,
}
#[derive(Debug, Clone)]
pub enum IrGroupOutput {
Groups,
Projection(Box<IrGroupProjection>),
Elements,
}
#[derive(Debug, Clone)]
pub struct IrPathSelect {
pub root: IrSource,
pub joins: Vec<IrPathJoin>,
pub result: IrPathResult,
pub filter: Option<IrExpr>,
pub order_by: Vec<IrSort>,
pub offset: Option<IrExpr>,
pub limit: Option<IrExpr>,
pub distinct: bool,
pub poly_implementors: Vec<IrPolyImplementor>,
}
#[derive(Debug, Clone)]
pub enum IrPathJoin {
Single {
source_alias: String,
fk_col: String,
target: IrSource,
},
Multi {
source_alias: String,
junction_alias: String,
join: IrMultiLinkJoin,
target: IrSource,
},
BacklinkSingle {
source_alias: String,
fk_col: String,
target: IrSource,
},
Function {
fn_module: String,
fn_name: String,
args: Vec<IrExpr>,
target: IrSource,
},
Lateral { inner: Box<IrPathSelect>, target: IrSource },
BacklinkMulti {
source_alias: String,
junction_alias: String,
junction_table: String,
module: String,
owner_col: String,
current_col: String,
target: IrSource,
},
}
#[derive(Debug, Clone)]
pub enum IrPathResult {
Scalar(IrExpr, Option<TupleCastShape>),
Object {
alias: String,
type_name: String,
shape: Vec<IrShapePointer>,
},
}
#[derive(Debug, Clone)]
pub enum IrFreeExpr {
Scalar(IrExpr),
FreeObject(Vec<(String, IrExpr)>),
NamedTupleRow(Vec<(String, IrExpr)>),
Tuple(Vec<IrExpr>),
AssertSet {
fn_name: String,
inner: Box<IrArraySource>,
message: Option<IrExpr>,
},
CtePassthrough(String),
}
#[derive(Debug, Clone)]
pub enum IrArraySource {
Select(IrSelect),
ObjectFunction(Box<IrFunctionSelect>),
ObjectSelect(Box<IrSelect>),
PathSelect(Box<IrPathSelect>),
Group(Box<IrGroup>),
StmtColumn {
stmt: Box<IrStmt>,
column: String,
},
RawExpr {
source: IrSource,
poly_implementors: Vec<IrPolyImplementor>,
poly_columns: Vec<String>,
expr: IrExpr,
},
}
#[derive(Debug, Clone)]
pub struct IrSelect {
pub rows: Vec<IrRowSource>,
pub filter: Option<IrExpr>,
pub order_by: Vec<IrSort>,
pub offset: Option<IrExpr>,
pub limit: Option<IrExpr>,
pub distinct: bool,
pub dml_source: Option<Box<IrStmt>>,
pub polymorphic: bool,
pub poly_implementors: Vec<IrPolyImplementor>,
pub poly_columns: Vec<String>,
pub lock: Option<IrLockClause>,
}
#[derive(Debug, Clone)]
pub struct IrLockClause {
pub strength: IrLockStrength,
pub wait: IrLockWait,
}
#[derive(Debug, Clone)]
pub enum IrLockStrength {
Update,
NoKeyUpdate,
Share,
KeyShare,
}
#[derive(Debug, Clone)]
pub enum IrLockWait {
Block,
NoWait,
SkipLocked,
}
#[derive(Debug, Clone)]
pub enum IrRowSource {
Bound {
source: IrSource,
shape: Vec<IrShapePointer>,
},
Free(IrFreeExpr),
}
impl IrSelect {
pub fn schema_bound(source: IrSource, shape: Vec<IrShapePointer>, filter: Option<IrExpr>) -> Self {
IrSelect {
rows: vec![IrRowSource::Bound { source, shape }],
filter,
order_by: vec![],
offset: None,
limit: None,
distinct: false,
dml_source: None,
polymorphic: false,
poly_implementors: vec![],
poly_columns: vec![],
lock: None,
}
}
}
#[derive(Debug, Clone)]
pub struct IrPolyImplementor {
pub type_name: String,
pub table: String,
pub module: String,
}
#[derive(Debug, Clone)]
pub struct IrSource {
pub type_name: String,
pub table: String,
pub alias: String,
pub poly: Option<IrPolyFanout>,
}
#[derive(Debug, Clone)]
pub struct IrPolyFanout {
pub implementors: Vec<IrPolyImplementor>,
pub columns: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct IrScalarSetPointer {
pub alias: String,
pub source: IrSource,
pub poly_implementors: Vec<IrPolyImplementor>,
pub poly_columns: Vec<String>,
pub bool_expr: IrExpr,
}
#[derive(Debug, Clone)]
pub enum IrShapePointer {
Scalar(IrScalarPointer),
SingleLink(IrSingleLinkPointer),
MultiLink(IrMultiLinkPointer),
Computed(IrComputedPointer),
ScalarSet(IrScalarSetPointer),
Asserted(Box<IrAssertedPointer>),
}
#[derive(Debug, Clone)]
pub struct IrAssertedPointer {
pub fn_name: String,
pub inner: IrShapePointer,
pub check: Option<IrShapePointer>,
pub message: Option<IrExpr>,
}
impl IrShapePointer {
pub fn alias(&self) -> &str {
match self {
IrShapePointer::Scalar(p) => &p.alias,
IrShapePointer::SingleLink(p) => &p.alias,
IrShapePointer::MultiLink(p) => &p.alias,
IrShapePointer::Computed(p) => &p.alias,
IrShapePointer::ScalarSet(p) => &p.alias,
IrShapePointer::Asserted(p) => p.inner.alias(),
}
}
pub fn is_object_pointer(&self) -> bool {
match self {
IrShapePointer::SingleLink(_) | IrShapePointer::MultiLink(_) => true,
IrShapePointer::Asserted(a) => a.inner.is_object_pointer(),
IrShapePointer::Computed(c) => match &c.expr {
IrExpr::ObjectSubquery(_) | IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. } => true,
IrExpr::ArrayFromSelect(source) => match source.as_ref() {
IrArraySource::ObjectFunction(_) | IrArraySource::ObjectSelect(_) => true,
IrArraySource::PathSelect(ps) => matches!(ps.result, IrPathResult::Object { .. }),
_ => false,
},
_ => false,
},
IrShapePointer::Scalar(_) | IrShapePointer::ScalarSet(_) => false,
}
}
}
#[derive(Debug, Clone)]
pub struct IrScalarPointer {
pub alias: String,
pub column: String,
pub pg_type: String,
pub tuple_shape: Option<TupleCastShape>,
pub marker_offset: Option<usize>,
pub implicit_id: bool,
}
#[derive(Debug, Clone)]
pub struct IrSingleLinkPointer {
pub alias: String,
pub correlation: IrSingleLinkCorrelation,
pub subquery: IrSelect,
pub link_properties: Vec<IrLinkProp>,
pub marker_offset: Option<usize>,
}
#[derive(Debug, Clone)]
pub enum IrSingleLinkCorrelation {
Fk {
fk_column: String,
target_pk: String,
},
Junction {
join: IrMultiLinkJoin,
target_pk: String,
},
}
#[derive(Debug, Clone)]
pub struct IrMultiLinkPointer {
pub alias: String,
pub join: IrMultiLinkJoin,
pub subquery: IrSelect,
pub link_properties: Vec<IrLinkProp>,
pub marker_offset: Option<usize>,
pub single: bool,
}
#[derive(Debug, Clone)]
pub struct IrLinkProp {
pub name: String,
}
#[derive(Debug, Clone)]
pub enum IrMultiLinkJoin {
Standard {
junction_table: String,
module: String,
},
Through {
junction_table: String,
module: String,
source_col: String,
target_col: String,
},
BacklinkFk {
fk_col: String,
},
BacklinkJunction {
junction_table: String,
module: String,
owner_col: String,
current_col: String,
},
}
#[derive(Debug, Clone)]
pub struct IrComputedPointer {
pub alias: String,
pub expr: IrExpr,
pub marker_offset: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct VectorEnqueueInfo {
pub type_name: String,
pub index_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SearchEnqueueInfo {
pub type_name: String,
pub index_name: Option<String>,
pub operation: &'static str,
pub backend: crate::schema::SearchBackend,
}
#[derive(Debug, Clone)]
pub struct IrInsert {
pub guard: Option<IrExpr>,
pub target: IrSource,
pub assignments: Vec<(String, IrExpr)>,
pub unless_conflict: Option<IrConflict>,
pub rewrites: Vec<IrRewrite>,
pub returning: Vec<IrShapePointer>,
pub enqueue_vector: Vec<VectorEnqueueInfo>,
pub enqueue_search: Vec<SearchEnqueueInfo>,
pub multi_link_appends: Vec<IrMultiLinkMutation>,
pub id_default_sql: Option<String>,
pub nested_ctes: Vec<IrCteDef>,
}
#[derive(Debug, Clone)]
pub struct IrConflict {
pub on: Option<IrExpr>,
pub do_update: Option<Vec<(String, IrExpr)>>,
pub do_update_where: Option<IrExpr>,
}
#[derive(Debug, Clone)]
pub struct IrUpdate {
pub target: IrSource,
pub filter: Option<IrExpr>,
pub assignments: Vec<(String, IrExpr)>,
pub rewrites: Vec<IrRewrite>,
pub returning: Vec<IrShapePointer>,
pub enqueue_vector: Vec<VectorEnqueueInfo>,
pub enqueue_search: Vec<SearchEnqueueInfo>,
pub poly_implementors: Vec<IrPolyImplementor>,
pub poly_columns: Vec<String>,
pub multi_link_clears: Vec<IrMultiLinkClear>,
pub multi_link_replaces: Vec<IrMultiLinkMutation>,
pub multi_link_appends: Vec<IrMultiLinkMutation>,
pub multi_link_removals: Vec<IrMultiLinkMutation>,
pub nested_ctes: Vec<IrCteDef>,
}
#[derive(Debug, Clone)]
pub struct IrMultiLinkClear {
pub junction_table: String,
pub module: String,
pub source_col: String,
}
#[derive(Debug, Clone)]
pub struct IrMultiLinkMutation {
pub junction_table: String,
pub module: String,
pub source_col: String,
pub target_col: String,
pub values: IrMultiLinkValues,
pub single: bool,
}
#[derive(Debug, Clone)]
pub struct IrMultiLinkValues {
pub source: IrMultiLinkValueSource,
pub link_props: Vec<(String, IrExpr)>,
}
#[derive(Debug, Clone)]
pub enum IrMultiLinkValueSource {
CteRef(String),
Select(Box<IrSelect>),
PathSelect(Box<IrPathSelect>),
Function(Box<IrFunctionSelect>),
Union(Box<IrMultiLinkValues>, Box<IrMultiLinkValues>),
Asserted {
fn_name: String,
inner: Box<IrMultiLinkValues>,
message: Option<IrExpr>,
},
}
#[derive(Debug, Clone)]
pub struct IrDelete {
pub target: IrSource,
pub filter: Option<IrExpr>,
pub returning: Vec<IrShapePointer>,
pub poly_implementors: Vec<IrPolyImplementor>,
pub poly_columns: Vec<String>,
pub enqueue_search: Vec<SearchEnqueueInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SetOpMode {
Array,
Exists,
Aggregate(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetOpKind {
Intersect,
Except,
}
impl SetOpKind {
pub fn sql(self) -> &'static str {
match self {
Self::Intersect => "INTERSECT",
Self::Except => "EXCEPT",
}
}
}
#[derive(Debug, Clone)]
pub enum IrExpr {
ColumnRef {
alias: String,
column: String,
pg_type: String,
},
Param {
index: usize,
},
Literal(IrLiteral),
BinOp(Box<IrBinOp>),
UnaryOp(Box<IrUnaryOp>),
FunctionCall(IrFunctionCall),
TypeCast(Box<IrTypeCast>),
IfElse(Box<IrIfElse>),
Subquery(Box<IrSelect>),
Array(Vec<IrExpr>),
Null,
AggOverSet {
fn_name: String,
schema: Option<String>,
elems: Vec<IrExpr>,
},
AggOverQuery {
fn_name: String,
inner: Box<IrSelect>,
},
AggOverCte {
fn_name: String,
cte: String,
column: Option<String>,
},
ExistsOverCte {
cte: String,
column: Option<String>,
},
SetOp {
op: SetOpKind,
left: Box<IrExpr>,
right: Box<IrExpr>,
mode: SetOpMode,
},
CteRef {
name: String,
scalar: bool,
pg_type: Option<String>,
},
CteFieldRef {
name: String,
field: String,
pg_type: Option<String>,
},
ForVar {
name: String,
pg_type: Option<String>,
},
ArrayFromSelect(Box<IrArraySource>),
ScalarSubquery(Box<IrSelect>),
EnumLiteral {
pg_type: String,
variant: String,
},
NamedTuple {
fields: Vec<(String, IrExpr)>,
is_free_object: bool,
},
ObjectPathUnion {
branches: Vec<IrPathSelect>,
limit: Option<Box<IrExpr>>,
multi: bool,
},
ObjectPathSubquery(Box<IrPathSelect>),
ObjectSubquery(Box<IrSelect>),
Tuple(Vec<IrExpr>),
GlobalParam {
index: usize,
pg_type: String,
},
GlobalRef {
cte_name: String,
},
Subscript {
expr: Box<IrExpr>,
index: Box<IrExpr>,
is_array: bool,
},
JsonbField {
expr: Box<IrExpr>,
field: String,
},
JsonbIndex {
expr: Box<IrExpr>,
index: usize,
},
Slice {
expr: Box<IrExpr>,
lower: Option<Box<IrExpr>>,
upper: Option<Box<IrExpr>>,
is_array: bool,
},
FnSubquery(Box<IrFunctionSelect>),
PathSubquery(Box<IrPathSelect>),
FnParam {
name: String,
pg_type: String,
},
RawSql(String),
}
#[derive(Debug, Clone)]
pub struct IrVectorSearch {
pub source: IrSource,
pub vector_col: String,
pub distance_op: &'static str,
pub query_expr: IrExpr,
pub object_shape: Vec<IrShapePointer>,
pub filter: Option<IrExpr>,
pub order_by_distance: Option<IrSortDir>,
pub offset: Option<IrExpr>,
pub limit: Option<IrExpr>,
pub inference_query_param_name: Option<String>,
pub inference_query_literal: Option<String>,
pub inference_model: Option<String>,
pub inference_type_name: Option<String>,
pub inference_index_name: Option<Option<String>>,
}
#[derive(Debug, Clone)]
pub struct IrFtsSearch {
pub source: IrSource,
pub backend: crate::schema::SearchBackend,
pub search_col: String,
pub tsquery_fn: &'static str,
pub query_expr: IrExpr,
pub object_shape: Vec<IrShapePointer>,
pub filter: Option<IrExpr>,
pub order_by_rank: Option<IrSortDir>,
pub offset: Option<IrExpr>,
pub limit: Option<IrExpr>,
pub deferred_index_name: Option<String>,
pub deferred_query_param_name: Option<String>,
pub deferred_query_literal: Option<String>,
pub deferred_ids_param: Option<usize>,
pub deferred_scores_param: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct IrFunctionSelect {
pub fn_module: String,
pub fn_name: String,
pub fn_args: Vec<IrExpr>,
pub alias: String,
pub type_name: String,
pub polymorphic: bool,
pub poly_implementors: Vec<IrPolyImplementor>,
pub poly_columns: Vec<String>,
pub shape: Vec<IrShapePointer>,
pub filter: Option<IrExpr>,
pub order_by: Vec<IrSort>,
pub offset: Option<IrExpr>,
pub limit: Option<IrExpr>,
pub distinct: bool,
}
#[derive(Debug, Clone)]
pub struct IrBinOp {
pub left: IrExpr,
pub op: BinOpKind,
pub right: IrExpr,
}
#[derive(Debug, Clone)]
pub struct IrUnaryOp {
pub op: UnaryOpKind,
pub operand: IrExpr,
}
#[derive(Debug, Clone)]
pub struct IrFunctionCall {
pub return_pg_type: Option<String>,
pub schema: Option<String>,
pub name: String,
pub args: Vec<IrExpr>,
pub sql_template: Option<String>,
}
#[derive(Debug, Clone)]
pub struct IrTypeCast {
pub expr: IrExpr,
pub pg_type: String,
pub tuple_shape: Option<TupleCastShape>,
}
#[derive(Debug, Clone)]
pub struct TupleCastShape {
pub type_name: Option<String>,
pub members: Vec<crate::query::JsonMember>,
}
#[derive(Debug, Clone)]
pub struct IrIfElse {
pub condition: IrExpr,
pub if_: IrExpr,
pub else_: IrExpr,
}
#[derive(Debug, Clone)]
pub enum IrLiteral {
Str(String),
Int(i64),
Float(f64),
Bool(bool),
}
#[derive(Debug, Clone)]
pub struct IrSort {
pub expr: IrExpr,
pub direction: IrSortDir,
pub nulls: IrNulls,
}
#[derive(Debug, Clone)]
pub enum IrSortDir {
Asc,
Desc,
}
#[derive(Debug, Clone)]
pub enum IrNulls {
First,
Last,
}
#[derive(Debug, Clone)]
pub struct IrRewrite {
pub column: String,
pub expr: IrExpr,
}
#[derive(Debug, Clone)]
pub struct IrCteDef {
pub name: String,
pub stmt: IrStmt,
pub type_name: String,
pub correlated_to: Option<String>,
}
#[derive(Debug, Clone)]
pub struct IrSessionGlobalCte {
pub cte_name: String,
pub qualified_name: String,
pub param_index: usize,
pub pg_type: String,
}
#[derive(Debug, Clone)]
pub struct IrComputedGlobalCte {
pub cte_name: String,
pub qualified_name: String,
pub stmt: IrStmt,
}
#[derive(Debug, Clone)]
pub enum IrGlobalCte {
Session(IrSessionGlobalCte),
Computed(Box<IrComputedGlobalCte>),
}
impl IrGlobalCte {
pub fn cte_name(&self) -> &str {
match self {
Self::Session(s) => &s.cte_name,
Self::Computed(c) => &c.cte_name,
}
}
}
pub struct IrOutput {
pub stmt: IrStmt,
pub params: Vec<String>,
pub ctes: Vec<IrCteDef>,
pub global_ctes: Vec<IrGlobalCte>,
pub warnings: Vec<String>,
pub uses_globals_arg: bool,
pub subtype_fanouts: HashMap<(String, String), IrPolyFanout>,
}
pub type QualifiedTable = (String, String);
const INHERITED_JUNCTION: &str = "@inherited:";
pub fn inherited_junction(tables: &[QualifiedTable], columns: &[String]) -> String {
let tables = tables
.iter()
.map(|(module, table)| format!("{module}\u{1f}{table}"))
.collect::<Vec<_>>()
.join("\u{1e}");
format!("{INHERITED_JUNCTION}{tables}\u{1d}{}", columns.join("\u{1f}"))
}
pub fn parse_inherited_junction(name: &str) -> Option<(Vec<QualifiedTable>, Vec<String>)> {
let (tables, columns) = name.strip_prefix(INHERITED_JUNCTION)?.split_once('\u{1d}')?;
let tables = tables
.split('\u{1e}')
.filter_map(|entry| entry.split_once('\u{1f}'))
.map(|(module, table)| (module.to_string(), table.to_string()))
.collect();
Some((tables, columns.split('\u{1f}').map(str::to_string).collect()))
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused_imports)]
use super::{IrFreeExpr, IrLiteral};
use crate::parse;
use crate::schema::{
ChannelDescriptor, ChannelPayload, ComputedDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
};
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(query: &str) -> IrOutput {
let schema = make_schema();
let ast = parse::parse(query).expect("parse failed");
super::compile(&ast, &schema).expect("IR compile failed")
}
fn bound(sel: &IrSelect) -> (&IrSource, &[IrShapePointer]) {
match sel.rows.as_slice() {
[IrRowSource::Bound { source, shape }] => (source, shape),
_ => panic!("expected a single schema-bound row"),
}
}
fn free_items(sel: &IrSelect) -> Vec<&IrFreeExpr> {
sel.rows
.iter()
.map(|r| match r {
IrRowSource::Free(item) => item,
IrRowSource::Bound { .. } => panic!("expected a free row"),
})
.collect()
}
#[test]
fn test_select_resolves_source() {
let ir = compile("SELECT Person { name, age }");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let (source, shape) = bound(&sel);
assert_eq!(source.table, "person");
assert_eq!(source.type_name, "default::Person");
assert_eq!(shape.len(), 3);
assert!(matches!(&shape[0], IrShapePointer::Scalar(p) if p.alias == "id" && p.implicit_id));
assert!(matches!(shape[1], IrShapePointer::Scalar(_)));
}
#[test]
fn test_select_filter_param_ordering() {
let ir = compile("SELECT Person { name } FILTER .name = $name AND .age > $min_age");
assert_eq!(ir.params, vec!["name", "min_age"]);
let IrStmt::Select(sel) = ir.stmt else { panic!() };
assert!(sel.filter.is_some());
}
#[test]
fn test_select_single_link() {
let ir = compile("SELECT Person { name, company { name } }");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let (_, shape) = bound(&sel);
assert_eq!(shape.len(), 3);
let IrShapePointer::SingleLink(link) = &shape[2] else {
panic!("expected SingleLink")
};
assert_eq!(link.alias, "company");
let IrSingleLinkCorrelation::Fk { fk_column, .. } = &link.correlation else {
panic!("expected Fk correlation")
};
assert_eq!(fk_column, "company_id");
assert_eq!(bound(&link.subquery).0.table, "company");
}
#[test]
fn test_select_multi_link() {
let ir = compile("SELECT Person { name, posts { title } }");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let (_, shape) = bound(&sel);
let IrShapePointer::MultiLink(ml) = &shape[2] else {
panic!("expected MultiLink")
};
assert_eq!(ml.alias, "posts");
assert_eq!(bound(&ml.subquery).0.table, "post");
let IrMultiLinkJoin::Standard { junction_table, .. } = &ml.join else {
panic!()
};
assert_eq!(junction_table, "person.posts");
}
#[test]
fn test_select_no_shape_returns_id_only() {
let ir = compile("SELECT Person");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let (_, shape) = bound(&sel);
assert_eq!(shape.len(), 1);
let IrShapePointer::Scalar(f) = &shape[0] else { panic!() };
assert_eq!(f.alias, "id");
}
#[test]
fn test_free_select_set_literal() {
let schema = make_schema();
let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
let ir = super::compile(&ast, &schema).unwrap();
let IrStmt::Select(sel) = ir.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
assert_eq!(items.len(), 3);
assert!(matches!(
items[0],
IrFreeExpr::Scalar(IrExpr::Literal(IrLiteral::Int(1)))
));
}
#[test]
fn test_free_select_free_object() {
let schema = make_schema();
let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
let ir = super::compile(&ast, &schema).unwrap();
let IrStmt::Select(sel) = ir.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
assert_eq!(items.len(), 1);
let IrFreeExpr::FreeObject(fields) = &items[0] else {
panic!("expected FreeObject")
};
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].0, "foo");
assert_eq!(fields[1].0, "n");
}
#[test]
fn test_free_select_tuple() {
let schema = make_schema();
let ast = parse::parse("SELECT (1, 'hello')").unwrap();
let ir = super::compile(&ast, &schema).unwrap();
let IrStmt::Select(sel) = ir.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
assert_eq!(items.len(), 1);
assert!(matches!(items[0], IrFreeExpr::Tuple(_)));
}
#[test]
fn test_free_select_scalar_literal() {
let schema = make_schema();
let ast = parse::parse("SELECT 42").unwrap();
let ir = super::compile(&ast, &schema).unwrap();
let IrStmt::Select(sel) = ir.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
assert_eq!(items.len(), 1);
assert!(matches!(
items[0],
IrFreeExpr::Scalar(IrExpr::Literal(IrLiteral::Int(42)))
));
}
#[test]
fn test_free_select_function_call() {
let schema = make_schema();
let ast = parse::parse("SELECT str_lower('HELLO')").unwrap();
let ir = super::compile(&ast, &schema).unwrap();
let IrStmt::Select(sel) = ir.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
assert!(matches!(items[0], IrFreeExpr::Scalar(IrExpr::FunctionCall(_))));
}
#[test]
fn test_free_select_rejects_dot_path() {
let schema = make_schema();
let ast = parse::parse("SELECT {.name}").unwrap();
assert!(super::compile(&ast, &schema).is_err());
}
#[test]
fn test_type_error_uuid_eq_str() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .id = 'not-a-uuid'").unwrap();
let err = super::compile(&ast, &schema).err().expect("expected type error");
let msg = err.to_string();
assert!(
msg.contains("std::uuid") && msg.contains("std::str"),
"unexpected: {msg}"
);
}
#[test]
fn test_type_error_str_eq_int() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .name = 42").unwrap();
let err = super::compile(&ast, &schema).err().expect("expected type error");
let msg = err.to_string();
assert!(
msg.contains("std::str") && msg.contains("std::int64"),
"unexpected: {msg}"
);
}
#[test]
fn test_int_literal_compatible_with_all_int_columns() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .age = 30").unwrap();
assert!(super::compile(&ast, &schema).is_ok());
}
#[test]
fn test_cast_int16_compatible_with_int8_column() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .age = <int16>30").unwrap();
assert!(super::compile(&ast, &schema).is_ok());
}
#[test]
fn test_unknown_type_error() {
let schema = make_schema();
let ast = parse::parse("SELECT Ghost { name }").unwrap();
assert!(super::compile(&ast, &schema).is_err());
}
#[test]
fn test_nested_dml_link_value_combines_with_multilink_mutation_in_the_same_update() {
let schema = make_schema();
let ast = parse::parse(
"UPDATE Person FILTER .id = $id SET { \
company := (select (insert Company { name := 'Acme' }) { id }), \
posts += (SELECT Post FILTER .title = $t) \
}",
)
.unwrap();
let ir = super::compile(&ast, &schema).unwrap();
let IrStmt::Update(upd) = ir.stmt else {
panic!("expected Update")
};
assert_eq!(upd.nested_ctes.len(), 1);
assert_eq!(upd.multi_link_appends.len(), 1);
}
#[test]
fn test_unknown_pointer_error() {
let schema = make_schema();
let ast = parse::parse("SELECT Person { nonexistent }").unwrap();
assert!(super::compile(&ast, &schema).is_err());
}
#[test]
fn test_insert_compiles_assignments() {
let ir = compile("INSERT Person { name := 'Alice', age := 30 }");
let IrStmt::Insert(ins) = ir.stmt else { panic!() };
assert_eq!(ins.target.table, "person");
assert_eq!(ins.assignments.len(), 2);
assert_eq!(ins.assignments[0].0, "name");
assert_eq!(ins.assignments[1].0, "age");
}
#[test]
fn test_delete_compiles_filter() {
let ir = compile("DELETE Person FILTER .name = $name");
let IrStmt::Delete(del) = ir.stmt else { panic!() };
assert!(del.filter.is_some());
assert_eq!(ir.params, vec!["name"]);
}
fn make_schema_with_computed() -> SchemaDescriptor {
let mut schema = make_schema();
schema.types[0].computed.push(ComputedDescriptor {
name: "upper_name".into(),
expression: "str_upper(.name)".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
});
schema
}
#[test]
fn test_computed_pointer_in_shape() {
let schema = make_schema_with_computed();
let ast = parse::parse("SELECT Person { upper_name }").unwrap();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let (_, shape) = bound(&sel);
assert!(
shape
.iter()
.any(|f| matches!(f, IrShapePointer::Computed(c) if c.alias == "upper_name"))
);
}
#[test]
fn test_computed_pointer_in_expression_context() {
let schema = make_schema_with_computed();
let ast = parse::parse("SELECT Person { x := str_lower(.upper_name) }").unwrap();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let (_, shape) = bound(&sel);
assert!(
shape
.iter()
.any(|f| matches!(f, IrShapePointer::Computed(c) if c.alias == "x"))
);
}
#[test]
fn test_count_over_multilink_in_computed_shape_element() {
let ir = compile("SELECT Person { post_count := count(.posts) }");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let (_, shape) = bound(&sel);
let computed = shape
.iter()
.find_map(|f| match f {
IrShapePointer::Computed(c) if c.alias == "post_count" => Some(c),
_ => None,
})
.expect("expected post_count computed pointer");
assert!(matches!(computed.expr, IrExpr::AggOverQuery { .. }));
}
#[test]
fn test_multi_sort_with_then() {
let ir = compile("SELECT Person { name } ORDER BY .name THEN .age");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
assert_eq!(sel.order_by.len(), 2);
}
#[test]
fn test_multi_link_filter_emits_warning() {
let ir = compile("SELECT Person { name } FILTER .posts.title = 'hello'");
assert!(!ir.warnings.is_empty(), "expected a warning for multi-link in filter");
assert!(ir.warnings[0].contains("posts"));
}
#[test]
fn test_session_global_produces_cte() {
let mut schema = make_schema();
schema.globals.push(GlobalDescriptor {
name: "viewer_id".into(),
module: "default".into(),
scalar_type: "std::uuid".into(),
required: false,
default_expr: None,
computed_expr: None,
});
let ast = parse::parse("SELECT Person FILTER .id = global viewer_id").unwrap();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
assert_eq!(ir.global_ctes.len(), 1);
assert_eq!(ir.global_ctes[0].cte_name(), "__global__default::viewer_id");
assert_eq!(ir.params, vec!["__global__default::viewer_id"]);
}
#[test]
fn test_session_global_pg_type_matches_pyql_type_name() {
let mut schema = make_schema();
schema.globals.push(GlobalDescriptor {
name: "viewer_id".into(),
module: "default".into(),
scalar_type: "std::uuid".into(),
required: false,
default_expr: None,
computed_expr: None,
});
let ast = parse::parse("SELECT Person FILTER .id = global viewer_id").unwrap();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrGlobalCte::Session(session) = &ir.global_ctes[0] else {
panic!("expected a session global CTE");
};
assert_eq!(session.pg_type, "uuid");
}
#[test]
fn test_computed_global_field_access_compiles_as_path_select() {
let mut schema = make_schema();
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 = <uuid>$session_user_id".into()),
});
let ast = parse::parse("SELECT global current_user.id").unwrap();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::PathSelect(sel) = ir.stmt else {
panic!("expected a path select, not a free select")
};
assert_eq!(sel.root.type_name, "default::Person");
}
#[test]
fn test_subquery_field_access_compiles_as_path_select() {
let ast = parse::parse("SELECT (SELECT default::Person FILTER .age > 20).name").unwrap();
let schema = make_schema();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::PathSelect(sel) = ir.stmt else {
panic!("expected a path select, not a free select")
};
assert_eq!(sel.root.type_name, "default::Person");
}
#[test]
fn test_string_index_compiles() {
let ast = parse::parse("SELECT 'hello'[1]").unwrap();
let schema = make_schema();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let items = free_items(&sel);
assert!(matches!(
items[0],
IrFreeExpr::Scalar(IrExpr::Subscript { is_array: false, .. })
));
}
#[test]
fn test_array_index_compiles() {
let ast = parse::parse("SELECT [1, 2, 3][0]").unwrap();
let schema = make_schema();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let items = free_items(&sel);
assert!(matches!(
items[0],
IrFreeExpr::Scalar(IrExpr::Subscript { is_array: true, .. })
));
}
#[test]
fn test_string_slice_compiles() {
let ast = parse::parse("SELECT 'hello'[1:3]").unwrap();
let schema = make_schema();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let items = free_items(&sel);
assert!(matches!(
items[0],
IrFreeExpr::Scalar(IrExpr::Slice { is_array: false, .. })
));
}
#[test]
fn test_array_slice_compiles() {
let ast = parse::parse("SELECT [1, 2, 3][0:2]").unwrap();
let schema = make_schema();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::Select(sel) = ir.stmt else { panic!() };
let items = free_items(&sel);
assert!(matches!(
items[0],
IrFreeExpr::Scalar(IrExpr::Slice { is_array: true, .. })
));
}
fn make_schema_with_alias() -> SchemaDescriptor {
use crate::schema::AliasDescriptor;
let mut schema = make_schema();
schema.aliases.push(AliasDescriptor {
name: "ActivePersons".into(),
module: "default".into(),
expr: "select Person filter .age >= 18".into(),
});
schema
}
fn make_schema_with_sequence() -> crate::schema::SchemaDescriptor {
use crate::schema::ScalarDescriptor;
let mut schema = make_schema();
schema.scalars.push(ScalarDescriptor {
name: "OrderNumber".into(),
module: "default".into(),
base: "Sequence".into(),
pg_type: "int8".into(),
check_constraints: vec![],
is_sequence: true,
});
schema
}
fn make_schema_with_channels() -> SchemaDescriptor {
let mut schema = make_schema();
schema.channels.push(ChannelDescriptor {
name: "Pings".into(),
module: "default".into(),
wire_name: "default__pings".into(),
payload: ChannelPayload::Scalar("text".into()),
description: None,
});
schema.channels.push(ChannelDescriptor {
name: "SearchReady".into(),
module: "default".into(),
wire_name: "default__search_ready".into(),
payload: ChannelPayload::Object(vec![
("doc_id".into(), "uuid".into()),
("score".into(), "float8".into()),
]),
description: None,
});
schema.channels.push(ChannelDescriptor {
name: "PersonUpdates".into(),
module: "default".into(),
wire_name: "default__person_updates".into(),
payload: ChannelPayload::Type("default::Person".into()),
description: None,
});
schema
}
fn compile_notify_expr(query: &str) -> String {
let schema = make_schema_with_channels();
let ast = parse::parse(query).expect("parse failed");
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::Select(sel) = ir.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
let IrFreeExpr::Scalar(expr) = items[0] else {
panic!("expected scalar")
};
crate::sql::emit_expr(expr)
}
fn notify_compile_err(query: &str) -> String {
let schema = make_schema_with_channels();
let ast = parse::parse(query).expect("parse failed");
format!(
"{}",
super::compile(&ast, &schema).err().expect("expected a compile error")
)
}
#[test]
fn test_notify_scalar_channel_emits_pg_notify() {
let sql = compile_notify_expr("SELECT notify(Pings, 'hello')");
assert_eq!(sql, "pg_notify('default__pings', ('hello')::text)", "got: {sql}");
}
#[test]
fn test_notify_rejects_unknown_channel() {
let err = notify_compile_err("SELECT notify(NoSuchChannel, 'hi')");
assert!(err.contains("not a known Channel"), "got: {err}");
}
#[test]
fn test_notify_object_channel_emits_jsonb_build_object() {
let sql = compile_notify_expr(
"SELECT notify(SearchReady, { doc_id := <uuid>'3fa85f64-5717-4562-b3fc-2c963f66afa6', score := 0.5 })",
);
assert_eq!(
sql,
"pg_notify('default__search_ready', (jsonb_build_object('doc_id', ('3fa85f64-5717-4562-b3fc-2c963f66afa6')::uuid, 'score', (0.5::float8)))::text)",
"got: {sql}"
);
}
#[test]
fn test_notify_object_channel_rejects_wrong_fields() {
let err = notify_compile_err("SELECT notify(SearchReady, { doc_id := 'x' })");
assert!(
err.contains("payload fields") && err.contains("don't match"),
"got: {err}"
);
}
#[test]
fn test_notify_object_channel_rejects_non_shape_payload() {
let err = notify_compile_err("SELECT notify(SearchReady, 'not an object')");
assert!(err.contains("free object literal"), "got: {err}");
}
#[test]
fn test_notify_type_channel_rejects_arbitrary_payload() {
let err = notify_compile_err("SELECT notify(PersonUpdates, 'not an anchor')");
assert!(err.contains("must name an object of that type"), "got: {err}");
}
#[test]
fn notify_composes_with_a_with_block_binding() {
let sql = compile_notify_expr(
"WITH updated := (UPDATE Person FILTER .id = <uuid>$id SET { name := 'x' }) \
SELECT notify(PersonUpdates, updated)",
);
assert!(sql.contains("pg_notify"), "got: {sql}");
assert!(sql.contains("\"id\""), "payload should be the CTE's id: {sql}");
assert!(sql.contains("updated"), "should reference the with-block CTE: {sql}");
}
#[test]
fn notify_rejects_a_with_block_binding_of_the_wrong_type() {
let err = notify_compile_err("WITH other := (SELECT Company) SELECT notify(PersonUpdates, other)");
assert!(err.contains("expects a payload of type"), "got: {err}");
}
#[test]
fn test_notify_type_channel_via_trigger_new_anchor() {
let schema = make_schema_with_channels();
let ir_out = super::compile_trigger_handler(
"select notify(PersonUpdates, __new__)",
"Person",
1, &schema,
)
.expect("trigger handler compile failed");
let IrStmt::Select(sel) = ir_out.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
let IrFreeExpr::Scalar(expr) = items[0] else {
panic!("expected scalar")
};
let sql = crate::sql::emit_expr(expr);
assert_eq!(
sql, "pg_notify('default__person_updates', (NEW.\"id\")::text)",
"got: {sql}"
);
}
#[test]
fn test_notify_scalar_channel_via_trigger_new_property_access() {
let schema = make_schema_with_channels();
let ir_out = super::compile_trigger_handler(
"select notify(Pings, __new__.name)",
"Person",
1, &schema,
)
.expect("trigger handler compile failed");
let IrStmt::Select(sel) = ir_out.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
let IrFreeExpr::Scalar(expr) = items[0] else {
panic!("expected scalar")
};
let sql = crate::sql::emit_expr(expr);
assert_eq!(sql, "pg_notify('default__pings', (NEW.\"name\")::text)", "got: {sql}");
}
#[test]
fn test_notify_type_channel_rejects_bare_reference_outside_trigger() {
let err = notify_compile_err("SELECT notify(PersonUpdates, __new__)");
assert!(err.contains("only bound inside a trigger handler"), "got: {err}");
}
#[test]
fn notify_rejects_an_oversized_concatenation_at_compile_time() {
let half = "x".repeat(4500);
let err = notify_compile_err(&format!("SELECT notify_raw('c', '{half}' ++ '{half}')"));
assert!(err.contains("8000-byte"), "got: {err}");
assert!(err.contains("at least"), "got: {err}");
}
#[test]
fn notify_allows_a_concatenation_that_still_fits() {
let part = "x".repeat(3000);
let sql = compile_notify_expr(&format!("SELECT notify_raw('c', '{part}' ++ '{part}')"));
assert!(sql.contains("pg_notify"), "got: {sql}");
}
#[test]
fn test_notify_raw_emits_pg_notify_with_two_args() {
let sql = compile_notify_expr("SELECT notify_raw('any_channel', 'raw payload')");
assert_eq!(sql, "pg_notify('any_channel', 'raw payload')", "got: {sql}");
}
#[test]
fn test_notify_payload_literal_over_cap_rejected() {
let huge = "x".repeat(8000);
let err = notify_compile_err(&format!("SELECT notify(Pings, '{huge}')"));
assert!(err.contains("NOTIFY payload limit"), "got: {err}");
}
#[test]
fn test_notify_arity_error() {
let err = notify_compile_err("SELECT notify(Pings)");
assert!(err.contains("takes exactly 2 arguments"), "got: {err}");
}
fn compile_seq(query: &str) -> String {
let schema = make_schema_with_sequence();
let ast = parse::parse(query).expect("parse failed");
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let IrStmt::Select(sel) = ir.stmt else {
panic!("expected Select")
};
let items = free_items(&sel);
let IrFreeExpr::Scalar(expr) = items[0] else {
panic!("expected scalar")
};
crate::sql::emit_expr(expr)
}
#[test]
fn test_sequence_next_emits_nextval() {
let sql = compile_seq("SELECT sequence_next(OrderNumber)");
assert_eq!(sql, r#"nextval('"default"."OrderNumber_seq"')"#, "got: {sql}");
}
#[test]
fn test_sequence_reset_no_val_emits_setval_initial() {
let sql = compile_seq("SELECT sequence_reset(OrderNumber)");
assert_eq!(sql, r#"setval('"default"."OrderNumber_seq"', 1, false)"#, "got: {sql}");
}
#[test]
fn test_sequence_reset_with_val_emits_setval() {
let sql = compile_seq("SELECT sequence_reset(OrderNumber, 1000)");
assert_eq!(
sql, r#"setval('"default"."OrderNumber_seq"', 1000, true)"#,
"got: {sql}"
);
}
#[test]
fn test_sequence_next_rejects_non_sequence_type() {
let schema = make_schema();
let ast = parse::parse("SELECT sequence_next(Person)").unwrap();
assert!(super::compile(&ast, &schema).is_err());
}
#[test]
fn test_alias_bare_compiles_to_type_select() {
let schema = make_schema_with_alias();
let ast = parse::parse("SELECT ActivePersons").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
assert!(sql.contains("18"), "expected age filter, got: {sql}");
}
#[test]
fn test_alias_with_outer_filter_merges() {
let schema = make_schema_with_alias();
let ast = parse::parse("SELECT ActivePersons FILTER .name = 'Alice'").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
assert!(sql.contains("18"), "expected alias filter, got: {sql}");
assert!(sql.contains("'Alice'"), "expected outer filter, got: {sql}");
}
#[test]
fn test_alias_module_qualified_resolves() {
let schema = make_schema_with_alias();
let ast = parse::parse("SELECT default::ActivePersons").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
}
#[test]
fn test_alias_with_shape() {
let schema = make_schema_with_alias();
let ast = parse::parse("SELECT ActivePersons { name, age }").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("\"name\""), "expected name pointer, got: {sql}");
assert!(sql.contains("\"age\""), "expected age pointer, got: {sql}");
}
#[test]
fn test_alias_whose_own_body_has_a_shape_plus_outer_shape() {
use crate::schema::AliasDescriptor;
let mut schema = make_schema();
schema.aliases.push(AliasDescriptor {
name: "OldestActive".into(),
module: "default".into(),
expr: "select Person { name } order by .age desc limit 1".into(),
});
let ast = parse::parse("SELECT OldestActive { name, age }").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("\"name\""), "expected name pointer, got: {sql}");
assert!(sql.contains("\"age\""), "expected age pointer, got: {sql}");
assert!(
sql.contains("ORDER BY") && sql.contains("LIMIT"),
"alias's own order/limit must still apply, got: {sql}"
);
}
#[test]
fn test_a_binding_named_like_a_generated_cte_gets_its_own_name() {
let ast = parse::parse("WITH _dml := (SELECT Person LIMIT 1) SELECT (UPDATE Person FILTER .id = _dml.id SET { age := 1 }) { name }").unwrap();
let ir = super::compile(&ast, &make_schema()).expect("IR compile failed");
let sql = crate::sql::emit(&ir).sql;
assert_eq!(
sql.matches("\"_dml\" AS (").count(),
1,
"the generated wrapper must keep the name to itself:\n{sql}"
);
}
#[test]
fn test_two_sibling_bindings_of_one_name_get_separate_with_names() {
let ast = parse::parse(
"WITH a := (FOR p IN (SELECT Person) UNION (WITH line := (SELECT Person) SELECT line.name)), \
b := (FOR p IN (SELECT Person) UNION (WITH line := (SELECT Post) SELECT line.title)) \
SELECT {a := a, b := b}",
)
.unwrap();
let ir = super::compile(&ast, &make_schema()).expect("IR compile failed");
let sql = crate::sql::emit(&ir).sql;
assert_eq!(
sql.matches("\"line\" AS (").count(),
1,
"one name can only be claimed once:\n{sql}"
);
}
#[test]
fn test_a_computed_inlined_twice_hoists_its_binding_once() {
let mut schema = make_schema();
schema.types[0].computed.push(ComputedDescriptor {
name: "ranked".into(),
expression: "(with ordering := ['a', 'b'] select array_get(ordering, 0))".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
});
let ast = parse::parse("SELECT Person { ranked } FILTER .ranked = 'a'").unwrap();
let ir = super::compile(&ast, &schema).expect("IR compile failed");
let sql = crate::sql::emit(&ir).sql;
assert_eq!(
sql.matches("\"ordering\" AS (").count(),
1,
"the shared binding must be hoisted once:\n{sql}"
);
}
#[test]
fn test_to_duration_takes_its_arguments_by_name() {
let schema = make_schema();
let ast = parse::parse("SELECT std::to_duration(seconds := 90.0)").unwrap();
super::compile(&ast, &schema).expect("named arguments must resolve");
}
#[test]
fn test_an_ordering_comparison_reads_a_set_walk_as_one_value() {
let schema = make_schema();
let ast = parse::parse("SELECT Company FILTER ((.<company[is Person].age < 30) ?? true)").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(!sql.contains("ARRAY(SELECT"), "the operand must be one value:\n{sql}");
assert!(sql.contains("COALESCE"), "the coalesce must survive:\n{sql}");
}
#[test]
fn test_a_single_link_walk_off_a_pinned_row_is_not_a_set() {
let schema = make_schema();
let ast =
parse::parse("WITH i := (SELECT Person FILTER .id = <uuid>$0) SELECT { c := i.company.name }").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(!sql.contains("ARRAY(SELECT"), "expected a value, not a set:\n{sql}");
}
#[test]
fn test_a_single_link_walk_off_an_unpinned_row_is_still_a_set() {
let schema = make_schema();
let ast = parse::parse("WITH i := (SELECT Person FILTER .name = 'x') SELECT { c := i.company.name }").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("ARRAY(SELECT"), "a walk off many rows is a set:\n{sql}");
}
#[test]
fn test_a_multi_link_step_is_a_set_even_off_a_pinned_row() {
let schema = make_schema();
let ast =
parse::parse("WITH i := (SELECT Person FILTER .id = <uuid>$0) SELECT { t := i.posts.title }").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("ARRAY(SELECT"), "a multi-link step is a set:\n{sql}");
}
#[test]
fn test_a_plain_read_does_not_see_the_statements_own_write() {
let schema = make_schema();
let ast =
parse::parse("WITH made := (INSERT Person { name := 'a', age := 1 }) SELECT { after := count(Person) }")
.unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(
sql.to_lowercase().contains("from \"public\".\"person\""),
"the count must read the table, not the CTE that wrote to it:\n{sql}"
);
}
#[test]
fn test_a_walk_off_a_mutation_sees_the_rows_it_just_wrote() {
let schema = make_schema();
let ast = parse::parse(
"SELECT (UPDATE Person FILTER .name = 'a' SET { posts += (INSERT Post { title := 't' }) }).posts { title }",
)
.unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(
sql.contains("__ml_add_0\" AS \"") || sql.contains("JOIN \"_nested_dml_1__ml_add_0\""),
"the junction must be read from the CTE that wrote it:\n{sql}"
);
assert!(
!sql.contains("JOIN \"public\".\"Post\""),
"the targets must come from their own CTE, not the base table:\n{sql}"
);
}
#[test]
fn test_a_type_intersection_may_follow_a_sub_select() {
let schema = make_schema();
let ast = parse::parse("SELECT (SELECT Person LIMIT 1).company[is Company].name").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(
!sql.contains("->'name'"),
"the field must not be jsonb off an id:\n{sql}"
);
assert!(sql.contains("\"name\""), "the field must be read as a column:\n{sql}");
}
#[test]
fn test_a_type_intersection_on_a_value_is_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT (1 + 2)[is Company]").unwrap();
let Err(error) = super::compile(&ast, &schema) else {
panic!("a type intersection on a number is not meaningful");
};
assert!(error.to_string().contains("needs a path, a binding"), "got: {error}");
}
#[test]
fn test_a_select_may_wrap_a_nested_union_subject_select() {
let schema = make_schema();
let ast =
parse::parse("SELECT (SELECT (Person.company UNION Person.company) { name } LIMIT 1) { name }").unwrap();
let error = super::compile(&ast, &schema)
.err()
.map(|e| e.to_string())
.unwrap_or_default();
assert!(
!error.contains("expected a type name as SELECT subject"),
"the union subject must be hoisted, got: {error}"
);
}
#[test]
fn test_a_select_subject_may_walk_off_a_mutation() {
let schema = make_schema();
let ast =
parse::parse("SELECT (UPDATE Person FILTER .name = 'a' SET { name := 'b' }).company { name }").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(
sql.contains("\"_nested_dml_0\" AS ("),
"the mutation must run as a CTE:\n{sql}"
);
assert!(sql.contains("UPDATE"), "the mutation must still run:\n{sql}");
}
#[test]
fn test_an_update_subject_may_walk_off_a_sub_select() {
let schema = make_schema();
let inline = parse::parse("UPDATE (SELECT Person FILTER .name = 'a').company SET { name := 'b' }").unwrap();
let bound =
parse::parse("WITH s := (SELECT Person FILTER .name = 'a') UPDATE s.company SET { name := 'b' }").unwrap();
let inline_err = super::compile(&inline, &schema).err().map(|e| e.to_string());
let bound_err = super::compile(&bound, &schema).err().map(|e| e.to_string());
assert_eq!(inline_err, bound_err, "the two spellings must compile alike");
assert!(
!inline_err
.unwrap_or_default()
.contains("expected a type name as SELECT subject"),
"the sub-select subject must be accepted"
);
}
#[test]
fn test_a_for_loop_may_be_a_select_subject() {
let schema = make_schema();
let ast = parse::parse("SELECT (FOR s IN {1, 2} UNION (SELECT { a := s }))").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
assert!(matches!(ir.stmt, super::IrStmt::For(_)), "expected the loop itself");
}
#[test]
fn test_a_json_cast_in_a_shape_is_a_named_pointer() {
use crate::query::ShapeNode;
let schema = make_schema();
let ast = parse::parse("SELECT Person { j := <json>.name }").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let shape = crate::sql::emit(&ir).shape;
let ShapeNode::Object { pointers, .. } = &shape.root else {
panic!("expected an object shape, got {:?}", shape.root);
};
let pointer = pointers
.iter()
.find(|node| matches!(node, ShapeNode::Scalar { name, .. } if name == "j"))
.unwrap_or_else(|| panic!("no scalar pointer named 'j' in {pointers:?}"));
assert!(matches!(pointer, ShapeNode::Scalar { .. }));
}
#[test]
fn test_a_top_level_json_cast_stays_a_root_shaped_node() {
use crate::query::ShapeNode;
let schema = make_schema();
let ast = parse::parse("SELECT <json>'x'").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let shape = crate::sql::emit(&ir).shape;
assert!(matches!(shape.root, ShapeNode::JsonScalar), "got {:?}", shape.root);
}
#[test]
fn test_stdlib_enum_member_compiles_to_a_text_literal() {
let schema = make_schema();
let ast = parse::parse("SELECT std::Endian.Big").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("'Big'::text"), "expected a text literal, got: {sql}");
}
#[test]
fn test_unknown_stdlib_enum_member_is_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT std::Endian.Middle").unwrap();
let Err(error) = super::compile(&ast, &schema) else {
panic!("Middle is not a member of std::Endian");
};
assert!(error.to_string().contains("has no member 'Middle'"), "got: {error}");
}
#[test]
fn test_to_bytes_of_a_uuid_selects_the_uuid_overload() {
let schema = make_schema();
let ast = parse::parse("SELECT std::to_int32(std::to_bytes(<uuid>$0)[12:16], std::Endian.Big)").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains("to_bytes_uuid"), "expected to_bytes_uuid, got: {sql}");
assert!(sql.contains("to_int32_bytes"), "expected to_int32_bytes, got: {sql}");
}
#[test]
fn a_stdlib_call_with_the_wrong_argument_count_is_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT std::str_lower('A', 'B')").unwrap();
let Err(err) = super::compile(&ast, &schema) else {
panic!("wrong arity must not compile")
};
let msg = err.to_string();
assert!(msg.contains("std::str_lower"), "{msg}");
assert!(msg.contains("takes 1 argument(s), got 2"), "{msg}");
}
#[test]
fn an_unknown_function_suggests_the_closest_real_one() {
let schema = make_schema();
let ast = parse::parse("SELECT std::uuid_generate_v7j()").unwrap();
let Err(err) = super::compile(&ast, &schema) else {
panic!("an unknown function must not compile")
};
let msg = err.to_string();
assert!(msg.contains("does not exist"), "{msg}");
assert!(msg.contains("did you mean std::uuid_generate_v7()?"), "{msg}");
}
#[test]
fn a_function_in_another_namespace_says_where_it_lives() {
let schema = make_schema();
let ast = parse::parse("SELECT std::pi()").unwrap();
let Err(err) = super::compile(&ast, &schema) else {
panic!("pi lives in math, not std")
};
assert!(err.to_string().contains("it lives in math, use math::pi()"), "{err}");
}
#[test]
fn an_arity_error_names_every_arity_the_overload_set_accepts() {
let schema = make_schema();
let ast = parse::parse("SELECT std::str_trim('A', 'B', 'C')").unwrap();
let Err(err) = super::compile(&ast, &schema) else {
panic!("wrong arity must not compile")
};
assert!(err.to_string().contains("takes 1 or 2 argument(s), got 3"), "{err}");
}
#[test]
fn a_variadic_stdlib_call_accepts_extra_arguments() {
let schema = make_schema();
for query in [
"SELECT std::json_get(<json>$0, 'a')",
"SELECT std::json_get(<json>$0, 'a', 'b', 'c')",
] {
let ast = parse::parse(query).unwrap();
super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
}
}
#[test]
fn a_stdlib_call_with_an_unacceptable_argument_type_is_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT std::str_lower(<int64>$0)").unwrap();
let Err(err) = super::compile(&ast, &schema) else {
panic!("wrong argument type must not compile")
};
let msg = err.to_string();
assert!(msg.contains("no overload accepting (int8)"), "{msg}");
assert!(msg.contains("(str)"), "{msg}");
}
#[test]
fn a_stdlib_call_over_a_coalesce_or_conditional_resolves_its_branch_type() {
let schema = make_schema();
for query in [
"SELECT std::str_lower(<optional str>$0 ?? 'DE')",
"SELECT std::str_lower('DE' ?? <optional str>$0)",
"WITH j := (<optional str>$0 ?? 'DE') SELECT std::str_lower(j)",
"SELECT std::str_lower(<str>$0 if <bool>$1 else 'DE')",
"SELECT std::len(<optional str>$0 ?? 'DE')",
] {
let ast = parse::parse(query).unwrap();
super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
}
}
#[test]
fn a_stdlib_call_over_a_pass_through_expression_resolves_the_value_type() {
let schema = make_schema();
for query in [
"SELECT (FOR code IN std::array_unpack(<array<str>>$0) UNION (SELECT std::str_lower(code)))",
"WITH x := { a := 'DE' } SELECT std::str_lower(x.a)",
"SELECT math::abs(-3)",
"SELECT math::abs(-(<int64>$0))",
"SELECT std::str_lower((<array<str>>$0)[0])",
"SELECT std::str_title(std::str_split(<str>$0, '::')[0])",
"SELECT std::duration_to_seconds(std::datetime_of_transaction() - <datetime>$0)",
"SELECT std::duration_to_seconds(<duration>$0 + <duration>$0)",
] {
let ast = parse::parse(query).unwrap();
super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
}
}
#[test]
fn a_stdlib_call_over_a_coalesce_of_the_wrong_type_is_still_rejected() {
let schema = make_schema();
let ast = parse::parse("SELECT std::str_lower(<optional int64>$0 ?? 3)").unwrap();
let Err(err) = super::compile(&ast, &schema) else {
panic!("wrong argument type must not compile")
};
assert!(err.to_string().contains("no overload accepting (int8)"), "{err}");
}
#[test]
fn test_to_int16_and_to_int64_over_bytes_select_the_bytes_overload() {
let schema = make_schema();
for (query, expected) in [
(
"SELECT std::to_int16(std::to_bytes(<uuid>$0)[14:16], std::Endian.Big)",
"to_int16_bytes",
),
(
"SELECT std::to_int64(std::to_bytes(<uuid>$0)[0:8], std::Endian.Little)",
"to_int64_bytes",
),
] {
let ast = parse::parse(query).unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
let sql = crate::sql::emit(&ir).sql;
assert!(sql.contains(expected), "expected {expected}, got: {sql}");
}
}
#[test]
fn test_positional_param_names() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .name = $0").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
assert_eq!(ir.params, vec!["0"]);
}
#[test]
fn test_multiple_positional_param_names_in_order() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .name = $0 AND .age > $1").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
assert_eq!(ir.params, vec!["0", "1"]);
}
#[test]
fn test_repeated_positional_param_single_slot() {
let schema = make_schema();
let ast = parse::parse("SELECT Person FILTER .name = $0 OR .name = $0").unwrap();
let ir = super::compile(&ast, &schema).expect("compile failed");
assert_eq!(ir.params, vec!["0"], "repeated $0 must occupy a single slot");
}
}