use std::collections::HashMap;
use helios_fhir::FhirVersion;
use helios_fhirpath::parse_expression;
use helios_fhirpath::parser::{Expression, Invocation, Literal, Term, TypeSpecifier};
use crate::core::sof_runner::SofError;
use super::ir::{
BinOp, BoundaryKind, BoundarySide, JsonPath, JsonType, LitValue, PathStep, SqlExpr, SqlType,
UnaryOp,
};
#[derive(Debug)]
pub struct CompileEnv {
pub root_alias: String,
pub next_param: usize,
pub constants: HashMap<String, Constant>,
pub param_bindings: Vec<LitValue>,
pub column_type_hint: Option<String>,
pub next_where_alias: usize,
pub resource_type: String,
pub fhir_version: FhirVersion,
}
#[derive(Debug, Clone)]
pub struct Constant {
pub value: LitValue,
pub bound_to: Option<usize>,
}
impl CompileEnv {
pub fn new(root_alias: impl Into<String>) -> Self {
Self {
root_alias: root_alias.into(),
next_param: 3,
constants: HashMap::new(),
param_bindings: Vec::new(),
column_type_hint: None,
next_where_alias: 0,
resource_type: String::new(),
fhir_version: FhirVersion::default_enabled(),
}
}
pub fn new_for_resource(
root_alias: impl Into<String>,
resource_type: impl Into<String>,
fhir_version: FhirVersion,
) -> Self {
let mut env = Self::new(root_alias);
env.resource_type = resource_type.into();
env.fhir_version = fhir_version;
env
}
}
const RESOURCE_ROOT: &str = "r.data";
pub fn compile_fhirpath_expr(src: &str, env: &mut CompileEnv) -> Result<SqlExpr, SofError> {
let trimmed = src.trim();
if trimmed.is_empty() {
return Err(SofError::Uncompilable {
reason: "empty FHIRPath expression".to_string(),
});
}
let parsed = parse_expression(trimmed).map_err(|e| SofError::Uncompilable { reason: e })?;
lower_expression(&parsed, env)
}
fn lower_expression(expr: &Expression, env: &mut CompileEnv) -> Result<SqlExpr, SofError> {
if let Some(scalar) = try_lower_where_scalar(expr, env)? {
return Ok(scalar);
}
if let Some(agg) = try_lower_join_aggregate(expr, env)? {
return Ok(agg);
}
match expr {
Expression::Term(term) => lower_term(term, env),
Expression::Invocation(base, inv) => lower_invocation(base, inv, env),
Expression::Indexer(base, idx) => lower_indexer(base, idx, env),
Expression::Polarity(sign, inner) => {
let inner_sql = lower_expression(inner, env)?;
Ok(match sign {
'+' => inner_sql,
'-' => SqlExpr::UnaryOp {
op: UnaryOp::Neg,
inner: Box::new(inner_sql),
},
other => {
return Err(SofError::Uncompilable {
reason: format!("unsupported polarity operator '{other}'"),
});
}
})
}
Expression::Equality(l, op, r) => {
let lhs = lower_expression(l, env)?;
let rhs = lower_expression(r, env)?;
let bin = match op.as_str() {
"=" => BinOp::Eq,
"!=" => BinOp::Neq,
"~" | "!~" => {
return Err(SofError::Uncompilable {
reason: format!(
"equivalence operator '{op}' is not supported by the in-DB runner"
),
});
}
other => {
return Err(SofError::Uncompilable {
reason: format!("unsupported equality operator '{other}'"),
});
}
};
Ok(SqlExpr::BinOp {
op: bin,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
})
}
Expression::Inequality(l, op, r) => {
let lhs = lower_expression(l, env)?;
let rhs = lower_expression(r, env)?;
let bin = match op.as_str() {
"<" => BinOp::Lt,
"<=" => BinOp::Lte,
">" => BinOp::Gt,
">=" => BinOp::Gte,
other => {
return Err(SofError::Uncompilable {
reason: format!("unsupported inequality operator '{other}'"),
});
}
};
Ok(SqlExpr::BinOp {
op: bin,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
})
}
Expression::And(l, r) => {
let lhs = lower_expression(l, env)?;
let rhs = lower_expression(r, env)?;
Ok(SqlExpr::BinOp {
op: BinOp::And,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
})
}
Expression::Or(l, op, r) => {
if op == "xor" {
return Err(SofError::Uncompilable {
reason: "xor operator is not supported by the in-DB runner".to_string(),
});
}
let lhs = lower_expression(l, env)?;
let rhs = lower_expression(r, env)?;
Ok(SqlExpr::BinOp {
op: BinOp::Or,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
})
}
Expression::Type(expr, op, ts) => lower_type_op(expr, op, ts, env),
Expression::Additive(l, op, r) => lower_arithmetic(l, op, r, env),
Expression::Multiplicative(l, op, r) => lower_arithmetic(l, op, r, env),
Expression::Union(_, _)
| Expression::Membership(_, _, _)
| Expression::Implies(_, _)
| Expression::Lambda(_, _)
| Expression::InstanceSelector(_, _) => Err(SofError::Uncompilable {
reason: format!("FHIRPath construct {expr:?} is not yet supported by the in-DB runner"),
}),
}
}
fn lower_arithmetic(
l: &Expression,
op: &str,
r: &Expression,
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
let lhs = lower_expression(l, env)?;
let rhs = lower_expression(r, env)?;
let bin = match op {
"+" => BinOp::Add,
"-" => BinOp::Sub,
"*" => BinOp::Mul,
"/" => BinOp::Div,
"div" | "mod" => {
return Err(SofError::Uncompilable {
reason: format!("integer-division operator '{op}' is not yet supported"),
});
}
other => {
return Err(SofError::Uncompilable {
reason: format!("unsupported arithmetic operator '{other}'"),
});
}
};
Ok(SqlExpr::BinOp {
op: bin,
lhs: Box::new(SqlExpr::Cast {
inner: Box::new(lhs),
ty: SqlType::Decimal,
}),
rhs: Box::new(SqlExpr::Cast {
inner: Box::new(rhs),
ty: SqlType::Decimal,
}),
})
}
fn lower_term(term: &Term, env: &mut CompileEnv) -> Result<SqlExpr, SofError> {
match term {
Term::Literal(lit) => lower_literal(lit),
Term::Invocation(inv) => lower_root_invocation(inv, env),
Term::Parenthesized(inner) => lower_expression(inner, env),
Term::ExternalConstant(name) => resolve_external_constant(name, env),
}
}
fn lower_literal(lit: &Literal) -> Result<SqlExpr, SofError> {
Ok(match lit {
Literal::Null => SqlExpr::Lit(LitValue::Null),
Literal::Boolean(b) => SqlExpr::Lit(LitValue::Bool(*b)),
Literal::Integer(n) => SqlExpr::Lit(LitValue::Int(*n)),
Literal::Number(d) => SqlExpr::Lit(LitValue::Decimal(d.to_string())),
Literal::String(s) => SqlExpr::Lit(LitValue::Str(s.clone())),
Literal::Date(_) | Literal::DateTime(_) | Literal::Time(_) | Literal::Quantity(_, _) => {
return Err(SofError::Uncompilable {
reason: format!("literal {lit:?} is not yet supported by the in-DB runner"),
});
}
})
}
fn lower_root_invocation(inv: &Invocation, env: &mut CompileEnv) -> Result<SqlExpr, SofError> {
match inv {
Invocation::Member(name) => Ok(SqlExpr::JsonPath {
root: env.root_alias.clone(),
path: JsonPath(vec![PathStep::Field(name.clone())]),
}),
Invocation::This => Ok(SqlExpr::JsonPath {
root: env.root_alias.clone(),
path: JsonPath::new(),
}),
Invocation::Function(name, args) => {
lower_function_call(
&SqlExpr::JsonPath {
root: env.root_alias.clone(),
path: JsonPath::new(),
},
name,
args,
env,
)
}
Invocation::Index | Invocation::Total => Err(SofError::Uncompilable {
reason: "$index / $total are not yet supported by the in-DB runner".to_string(),
}),
}
}
fn lower_invocation(
base: &Expression,
inv: &Invocation,
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
if let Invocation::Function(term, term_args) = inv
&& term_args.is_empty()
&& (term == "exists" || term == "empty")
{
if let Expression::Invocation(inner_base, Invocation::Function(name, args)) = base
&& name == "where"
&& args.len() == 1
{
return lower_where_exists(Some(inner_base), &args[0], term == "empty", env);
}
if let Expression::Term(Term::Invocation(Invocation::Function(name, args))) = base
&& name == "where"
&& args.len() == 1
{
return lower_where_exists(None, &args[0], term == "empty", env);
}
}
let base_sql = lower_expression(base, env)?;
match inv {
Invocation::Member(name) => extend_path(base_sql, PathStep::Field(name.clone()), env),
Invocation::Function(name, args) => lower_function_call(&base_sql, name, args, env),
Invocation::This => Ok(base_sql),
Invocation::Index | Invocation::Total => Err(SofError::Uncompilable {
reason: "$index / $total are not yet supported by the in-DB runner".to_string(),
}),
}
}
fn try_lower_where_scalar(
expr: &Expression,
env: &mut CompileEnv,
) -> Result<Option<SqlExpr>, SofError> {
let mut steps: Vec<PostStep> = Vec::new();
let mut cur = expr;
loop {
match cur {
Expression::Invocation(base, Invocation::Member(name)) => {
steps.push(PostStep::Field(name.clone()));
cur = base;
}
Expression::Invocation(base, Invocation::Function(name, args))
if name == "first" && args.is_empty() =>
{
cur = base;
}
Expression::Invocation(base, Invocation::Function(name, args))
if name == "ofType" && args.len() == 1 =>
{
let ty = type_name_from_arg(&args[0])?;
steps.push(PostStep::OfType(ty));
cur = base;
}
Expression::Indexer(base, idx) => {
if let Expression::Term(Term::Literal(Literal::Integer(n))) = idx.as_ref() {
steps.push(PostStep::Index(*n));
cur = base;
} else {
return Ok(None);
}
}
Expression::Invocation(inner_base, Invocation::Function(name, args))
if name == "where" && args.len() == 1 =>
{
if steps.is_empty() {
return Ok(None);
}
return Ok(Some(build_where_scalar(
inner_base, None, &args[0], steps, env,
)?));
}
Expression::Invocation(inner_base, Invocation::Function(name, args))
if name == "extension" && args.len() == 1 =>
{
if steps.is_empty() {
return Ok(None);
}
let url_arg = args[0].clone();
return Ok(Some(build_where_scalar(
inner_base,
Some("extension".to_string()),
&url_arg,
steps,
env,
)?));
}
Expression::Term(Term::Invocation(Invocation::Function(name, args)))
if name == "extension" && args.len() == 1 =>
{
if steps.is_empty() {
return Ok(None);
}
let url_arg = args[0].clone();
return Ok(Some(build_where_scalar_at_root(
Some("extension".to_string()),
&url_arg,
steps,
env,
)?));
}
_ => return Ok(None),
}
}
}
fn build_where_scalar(
base: &Expression,
sugar_field: Option<String>,
crit_or_url: &Expression,
steps: Vec<PostStep>,
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
let is_ext = sugar_field.is_some();
let mut focus = lower_expression(base, env)?;
if let Some(field) = sugar_field {
focus = extend_path(focus, PathStep::Field(field), env)?;
}
finish_where_scalar(focus, crit_or_url, steps, is_ext, env)
}
fn build_where_scalar_at_root(
sugar_field: Option<String>,
crit_or_url: &Expression,
steps: Vec<PostStep>,
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
let is_ext = sugar_field.is_some();
let mut focus = SqlExpr::JsonPath {
root: env.root_alias.clone(),
path: super::ir::JsonPath::new(),
};
if let Some(field) = sugar_field {
focus = extend_path(focus, PathStep::Field(field), env)?;
}
finish_where_scalar(focus, crit_or_url, steps, is_ext, env)
}
fn finish_where_scalar(
focus: SqlExpr,
crit_or_url: &Expression,
steps: Vec<PostStep>,
is_extension_sugar: bool,
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
let alias = format!("w{}", env.next_where_alias);
env.next_where_alias += 1;
let prev_root = env.root_alias.clone();
env.root_alias = format!("{alias}.value");
let predicate = if is_extension_sugar {
let url_path = SqlExpr::JsonPath {
root: env.root_alias.clone(),
path: super::ir::JsonPath(vec![PathStep::Field("url".to_string())]),
};
let rhs = lower_expression(crit_or_url, env);
let rhs_expr = match rhs {
Ok(e) => e,
Err(e) => {
env.root_alias = prev_root;
return Err(e);
}
};
SqlExpr::BinOp {
op: BinOp::Eq,
lhs: Box::new(url_path),
rhs: Box::new(rhs_expr),
}
} else {
match lower_expression(crit_or_url, env) {
Ok(e) => e,
Err(e) => {
env.root_alias = prev_root;
return Err(e);
}
}
};
let mut projection = SqlExpr::JsonPath {
root: env.root_alias.clone(),
path: super::ir::JsonPath::new(),
};
for step in steps.into_iter().rev() {
projection = match step {
PostStep::Field(name) => extend_path(projection, PathStep::Field(name), env)?,
PostStep::Index(n) => extend_path(projection, PathStep::Index(n), env)?,
PostStep::OfType(t) => extend_path(projection, PathStep::OfType(t), env)?,
};
}
env.root_alias = prev_root;
Ok(SqlExpr::WhereScalar {
focus: Box::new(focus),
iter_alias: alias,
predicate: Box::new(predicate),
projection: Box::new(projection),
})
}
#[derive(Debug)]
enum PostStep {
Field(String),
Index(i64),
OfType(String),
}
fn try_lower_join_aggregate(
expr: &Expression,
env: &mut CompileEnv,
) -> Result<Option<SqlExpr>, SofError> {
let (call_base, sep_arg_opt) = match expr {
Expression::Invocation(b, Invocation::Function(name, args))
if name == "join" && args.len() <= 1 =>
{
(b.as_ref(), args.first())
}
_ => return Ok(None),
};
let (outer_base_expr, inner_field) = match call_base {
Expression::Invocation(b, Invocation::Member(field)) => (b.as_ref(), field.clone()),
_ => return Ok(None),
};
let sep = match sep_arg_opt {
None => String::new(),
Some(Expression::Term(Term::Literal(Literal::String(s)))) => s.clone(),
Some(_) => return Ok(None),
};
let outer_focus = lower_expression(outer_base_expr, env)?;
let outer_alias = format!("ja{}", env.next_where_alias);
env.next_where_alias += 1;
let inner_alias = format!("ja{}", env.next_where_alias);
env.next_where_alias += 1;
Ok(Some(SqlExpr::JoinAggregate {
outer_focus: Box::new(outer_focus),
outer_alias,
inner_field,
inner_alias,
separator: sep,
}))
}
fn lower_where_exists(
base: Option<&Expression>,
crit: &Expression,
negate: bool,
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
if base.is_none() {
let predicate = lower_expression(crit, env)?;
return Ok(if negate {
SqlExpr::UnaryOp {
op: UnaryOp::Not,
inner: Box::new(predicate),
}
} else {
predicate
});
}
let focus = lower_expression(base.unwrap(), env)?;
let alias = format!("w{}", env.next_where_alias);
env.next_where_alias += 1;
let prev_root = env.root_alias.clone();
env.root_alias = format!("{alias}.value");
let predicate = lower_expression(crit, env);
env.root_alias = prev_root;
let predicate = predicate?;
Ok(SqlExpr::WhereExists {
focus: Box::new(focus),
iter_alias: alias,
predicate: Box::new(predicate),
negate,
})
}
fn lower_indexer(
base: &Expression,
idx: &Expression,
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
let base_sql = lower_expression(base, env)?;
let idx_n = match idx {
Expression::Term(Term::Literal(Literal::Integer(n))) => *n,
Expression::Term(Term::ExternalConstant(name)) => {
match env.constants.get(name).map(|c| c.value.clone()) {
Some(LitValue::Int(n)) => n,
Some(other) => {
return Err(SofError::Uncompilable {
reason: format!(
"constant '%{name}' used as array index must be an integer (got {other:?})"
),
});
}
None => {
return Err(SofError::InvalidViewDefinition(format!(
"FHIRPath references undefined constant '%{name}' \
in array index position"
)));
}
}
}
_ => {
return Err(SofError::Uncompilable {
reason: "only integer-literal or %integer-constant index expressions are \
supported by the in-DB runner"
.to_string(),
});
}
};
extend_path(base_sql, PathStep::Index(idx_n), env)
}
fn extend_path(base: SqlExpr, step: PathStep, env: &CompileEnv) -> Result<SqlExpr, SofError> {
match base {
SqlExpr::JsonPath { root, mut path } => {
if let PathStep::OfType(type_name) = &step
&& let Some(PathStep::Field(prev)) = path.0.last()
{
let variant = format!("{prev}{}", uppercase_first(type_name));
if polymorphic_variant_exists(&root, &path, &variant, env) {
let last = path.0.len() - 1;
path.0[last] = PathStep::Field(variant);
return Ok(SqlExpr::JsonPath { root, path });
}
}
path.push(step);
Ok(SqlExpr::JsonPath { root, path })
}
SqlExpr::WhereScalar {
focus,
iter_alias,
predicate,
projection,
} => {
let new_projection = extend_path(*projection, step, env)?;
Ok(SqlExpr::WhereScalar {
focus,
iter_alias,
predicate,
projection: Box::new(new_projection),
})
}
other => Err(SofError::Uncompilable {
reason: format!("cannot extend non-path expression {other:?} with a path step"),
}),
}
}
fn polymorphic_variant_exists(
root: &str,
path: &JsonPath,
variant: &str,
env: &CompileEnv,
) -> bool {
match parent_type_of_last_field(root, path, env) {
Some(parent) => super::lookup_field_type(env.fhir_version, &parent, variant).is_some(),
None => super::field_exists_anywhere(env.fhir_version, variant),
}
}
fn parent_type_of_last_field(root: &str, path: &JsonPath, env: &CompileEnv) -> Option<String> {
if root != RESOURCE_ROOT || env.resource_type.is_empty() {
return None;
}
let last_field_pos = path
.0
.iter()
.rposition(|s| matches!(s, PathStep::Field(_)))?;
let mut parent = env.resource_type.clone();
for step in &path.0[..last_field_pos] {
match step {
PathStep::Field(name) => {
let (ty, _) = super::lookup_field_type(env.fhir_version, &parent, name)?;
parent = ty.to_string();
}
PathStep::Index(_) => {}
PathStep::OfType(t) => parent = t.clone(),
PathStep::TypeFilter(_) => return None,
}
}
Some(parent)
}
fn uppercase_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn lower_function_call(
focus: &SqlExpr,
name: &str,
args: &[Expression],
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
match name {
"exists" if args.is_empty() => Ok(SqlExpr::UnaryOp {
op: UnaryOp::IsNotNull,
inner: Box::new(focus.clone()),
}),
"empty" if args.is_empty() => Ok(SqlExpr::UnaryOp {
op: UnaryOp::IsNull,
inner: Box::new(focus.clone()),
}),
"not" if args.is_empty() => Ok(SqlExpr::UnaryOp {
op: UnaryOp::Not,
inner: Box::new(focus.clone()),
}),
"first" if args.is_empty() => {
match focus {
SqlExpr::JsonPath { root, path } => {
let mut new_path = path.clone();
new_path.push(PathStep::Index(0));
Ok(SqlExpr::JsonPath {
root: root.clone(),
path: new_path,
})
}
_ => Ok(focus.clone()),
}
}
"last" if args.is_empty() => {
Ok(focus.clone())
}
"iif" if (args.len() == 2 || args.len() == 3) => {
let cond = lower_expression(&args[0], env)?;
let then_expr = lower_expression(&args[1], env)?;
let else_expr = if args.len() == 3 {
Some(Box::new(lower_expression(&args[2], env)?))
} else {
None
};
Ok(SqlExpr::Case {
arms: vec![(cond, then_expr)],
else_: else_expr,
})
}
"ofType" if args.len() == 1 => {
let ty = type_name_from_arg(&args[0])?;
extend_path(focus.clone(), PathStep::OfType(ty), env)
}
"getResourceKey" if args.is_empty() => {
extend_path(focus.clone(), PathStep::Field("id".to_string()), env)
}
"getReferenceKey" if args.is_empty() => {
let reference =
extend_path(focus.clone(), PathStep::Field("reference".to_string()), env)?;
Ok(SqlExpr::ReferenceKey {
reference: Box::new(reference),
expected_type: None,
})
}
"getReferenceKey" if args.len() == 1 => {
let expected = type_name_from_arg(&args[0])?;
let reference =
extend_path(focus.clone(), PathStep::Field("reference".to_string()), env)?;
Ok(SqlExpr::ReferenceKey {
reference: Box::new(reference),
expected_type: Some(expected),
})
}
"count" if args.is_empty() => {
Ok(SqlExpr::Case {
arms: vec![(
SqlExpr::UnaryOp {
op: UnaryOp::IsNotNull,
inner: Box::new(focus.clone()),
},
SqlExpr::Lit(LitValue::Int(1)),
)],
else_: Some(Box::new(SqlExpr::Lit(LitValue::Int(0)))),
})
}
"join" if args.len() <= 1 => {
Ok(focus.clone())
}
"extension" if args.len() == 1 => {
let alias = format!("w{}", env.next_where_alias);
env.next_where_alias += 1;
let ext_focus =
extend_path(focus.clone(), PathStep::Field("extension".to_string()), env)?;
let prev_root = env.root_alias.clone();
env.root_alias = format!("{alias}.value");
let url_path = SqlExpr::JsonPath {
root: env.root_alias.clone(),
path: super::ir::JsonPath(vec![PathStep::Field("url".to_string())]),
};
let url_arg = lower_expression(&args[0], env);
let projection = SqlExpr::JsonPath {
root: env.root_alias.clone(),
path: super::ir::JsonPath::new(),
};
env.root_alias = prev_root;
let url_arg = url_arg?;
Ok(SqlExpr::WhereScalar {
focus: Box::new(ext_focus),
iter_alias: alias,
predicate: Box::new(SqlExpr::BinOp {
op: BinOp::Eq,
lhs: Box::new(url_path),
rhs: Box::new(url_arg),
}),
projection: Box::new(projection),
})
}
"lowBoundary" if args.is_empty() => Ok(SqlExpr::Boundary {
side: BoundarySide::Low,
kind: boundary_kind_from_hint(env)?,
source: Box::new(focus.clone()),
}),
"highBoundary" if args.is_empty() => Ok(SqlExpr::Boundary {
side: BoundarySide::High,
kind: boundary_kind_from_hint(env)?,
source: Box::new(focus.clone()),
}),
other => Err(SofError::Uncompilable {
reason: format!(
"FHIRPath function {other}({}) is not yet supported by the in-DB runner",
args.len()
),
}),
}
}
fn type_name_from_arg(arg: &Expression) -> Result<String, SofError> {
match arg {
Expression::Term(Term::Invocation(Invocation::Member(name))) => Ok(name.clone()),
_ => Err(SofError::Uncompilable {
reason: format!("ofType() argument must be a bare type identifier (got {arg:?})"),
}),
}
}
fn boundary_kind_from_hint(env: &CompileEnv) -> Result<BoundaryKind, SofError> {
match env.column_type_hint.as_deref() {
Some("decimal") | Some("integer") | Some("positiveInt") | Some("unsignedInt") => {
Ok(BoundaryKind::Decimal)
}
Some("date") => Ok(BoundaryKind::Date),
Some("dateTime") | Some("instant") => Ok(BoundaryKind::DateTime),
Some("time") => Ok(BoundaryKind::Time),
Some(other) => Err(SofError::Uncompilable {
reason: format!(
"lowBoundary()/highBoundary() requires a column.type of decimal/date/dateTime/time \
to disambiguate the source value type (got '{other}')"
),
}),
None => Err(SofError::Uncompilable {
reason: "lowBoundary()/highBoundary() requires the enclosing column to declare a \
`type` so the compiler can pick decimal vs. date/dateTime/time semantics"
.to_string(),
}),
}
}
fn resolve_external_constant(name: &str, env: &mut CompileEnv) -> Result<SqlExpr, SofError> {
let constant = env.constants.get(name).cloned().ok_or_else(|| {
SofError::InvalidViewDefinition(format!(
"FHIRPath references undefined constant '%{name}' (not declared in ViewDefinition.constant[])"
))
})?;
if let Some(idx) = constant.bound_to {
return Ok(SqlExpr::Param(idx));
}
let idx = env.next_param;
env.next_param += 1;
env.param_bindings.push(constant.value.clone());
if let Some(slot) = env.constants.get_mut(name) {
slot.bound_to = Some(idx);
}
Ok(SqlExpr::Param(idx))
}
fn lower_type_op(
expr: &Expression,
op: &str,
ts: &TypeSpecifier,
env: &mut CompileEnv,
) -> Result<SqlExpr, SofError> {
let TypeSpecifier::QualifiedIdentifier(a, b) = ts;
let type_name = match b {
Some(t) => t.clone(),
None => a.clone(),
};
let base = lower_expression(expr, env)?;
match op {
"is" => {
let _ = base;
let _ = type_name;
Err(SofError::Uncompilable {
reason: "'is' operator is not yet implemented in the in-DB runner".to_string(),
})
}
"as" => extend_path(base, PathStep::OfType(type_name), env),
other => Err(SofError::Uncompilable {
reason: format!("unsupported type operator '{other}'"),
}),
}
}
const _: Option<JsonType> = None;
const _: Option<SqlType> = None;