use crate::error::{Position, PyQLError, PyQLFragmentError};
use crate::ir::{
IrFreeExpr, IrRowSource, IrStmt, compile, compile_fn_body, compile_scalar_default_typed, compile_trigger_handler,
infer_ir_type, types_compatible,
};
use crate::schema::SchemaDescriptor;
fn multi_valued(expr: &crate::ir::IrExpr, schema: &SchemaDescriptor) -> Option<String> {
use crate::ir::IrExpr as E;
match expr {
E::FunctionCall(f) if f.schema.is_none() && f.name == "coalesce" => {
f.args.first().and_then(|a| multi_valued(a, schema))
}
E::TypeCast(c) => multi_valued(&c.expr, schema),
E::ArrayFromSelect(_) => Some("a path that crosses a multilink, so it yields many values".into()),
E::SetOp { mode, .. } if *mode == crate::ir::SetOpMode::Array => {
Some("a set operation, so it yields many values".into())
}
E::FunctionCall(f) => {
let module = f.schema.as_deref()?;
let fd = schema
.functions
.iter()
.find(|d| d.module == module && d.name == f.name && d.return_is_set)?;
Some(format!(
"a call to set-returning function '{}::{}', so it yields many values",
fd.module, fd.name
))
}
_ => None,
}
}
fn mismatch(context: String, message: String) -> PyQLError {
PyQLError::Fragment(PyQLFragmentError {
message,
position: Position { line: 0, col: 0 },
context,
})
}
const PARTITIONABLE_PG_TYPES: [&str; 3] = ["timestamptz", "timestamp", "date"];
pub fn validate_partitions(schema: &SchemaDescriptor) -> Vec<PyQLError> {
let mut errors = Vec::new();
for td in &schema.types {
let Some(part) = &td.partition else { continue };
let type_name = format!("{}::{}", td.module, td.name);
let context = format!("{type_name} (partition)");
if td.abstract_ && !td.materialized {
errors.push(mismatch(
context.clone(),
format!(
"type '{type_name}' is abstract and has no table of its own, so it cannot declare a Partition — \
declare it on each concrete type instead"
),
));
continue;
}
if td.abstract_ && td.materialized {
errors.push(mismatch(
context.clone(),
format!(
"type '{type_name}' is an interface, backed by a view rather than a table, so it cannot declare \
a Partition — declare it on each implementing type instead"
),
));
continue;
}
let Some(prop) = td.properties.iter().find(|p| p.name == part.pointer) else {
errors.push(mismatch(
context.clone(),
format!(
"Partition on '{type_name}' names pointer '{}', which is not a property of this type",
part.pointer
),
));
continue;
};
if !PARTITIONABLE_PG_TYPES.contains(&prop.pg_type.as_str()) {
errors.push(mismatch(
context.clone(),
format!(
"Partition on '{type_name}' names property '{}' of type '{}' — the partition key must be a \
datetime or date property",
part.pointer, prop.pg_type
),
));
}
if prop.nullable {
errors.push(mismatch(
context.clone(),
format!(
"Partition on '{type_name}' names optional property '{}' — a partition key can never be empty",
part.pointer
),
));
}
if part.premake == 0 {
errors.push(mismatch(
context.clone(),
format!(
"Partition on '{type_name}' has premake=0 — with no future partitions pre-created, the first \
write past the current range fails"
),
));
}
}
errors
}
pub fn validate_schema_types(schema: &SchemaDescriptor) -> Result<(), Vec<PyQLError>> {
let mut errors = validate_partitions(schema);
for fd in &schema.functions {
if fd.return_is_object {
continue;
}
let context = format!("{}::{}", fd.module, fd.name);
let ir_output = match compile_fn_body(fd, schema) {
Ok(o) => o,
Err(e) => {
errors.push(e);
continue;
}
};
let IrStmt::Select(sel) = &ir_output.stmt else { continue };
let [IrRowSource::Free(IrFreeExpr::Scalar(e))] = sel.rows.as_slice() else {
continue;
};
let Some(actual) = infer_ir_type(e) else { continue };
if !types_compatible(actual, &fd.return_pg_type) {
errors.push(mismatch(
context.clone(),
format!(
"return type mismatch in function '{}': declared {}, body produces {}",
context, fd.return_pg_type, actual
),
));
}
}
for td in &schema.types {
let type_name = format!("{}::{}", td.module, td.name);
for cd in &td.computed {
let Some(declared) = &cd.return_type else { continue };
let context = format!("{}.{} (computed)", type_name, cd.name);
let ir = match crate::ir::compile_computed_in_type(cd, &type_name, schema) {
Ok(Some(ir)) => ir,
Ok(None) => continue,
Err(e) => {
errors.push(e);
continue;
}
};
if !declared.ends_with("[]")
&& let Some(why) = multi_valued(&ir, schema)
{
errors.push(mismatch(
context.clone(),
format!(
"cardinality mismatch in computed pointer '{context}': declared {declared}, a single \
value, but the expression is {why} — declare it as an array \
(e.g. Computed[pylon.Array[...], …]) or reduce it to one value \
(e.g. with 'limit 1', 'assert_single()', or an aggregate)"
),
));
continue;
}
let Some(actual) = infer_ir_type(&ir) else { continue };
if !types_compatible(actual, declared) {
errors.push(mismatch(
context.clone(),
format!(
"return type mismatch in computed pointer '{}': declared {}, expression produces {}",
context, declared, actual
),
));
}
}
for prop in &td.properties {
let Some(pyql) = &prop.default_pyql else { continue };
let context = format!("{}.{} (default)", type_name, prop.name);
let Ok((_, ir)) = compile_scalar_default_typed(pyql, schema) else {
continue;
};
if crate::ir::default_blocker(&ir).is_some() {
continue;
}
let Some(actual) = infer_ir_type(&ir) else { continue };
if !types_compatible(actual, &prop.pg_type) {
errors.push(mismatch(
context.clone(),
format!(
"default value type mismatch for '{}': expected {}, default produces {}",
context, prop.pg_type, actual
),
));
}
}
for link in &td.links {
let Some(pyql) = &link.default_pyql else { continue };
let context = format!("{}.{} (default)", type_name, link.name);
let Ok((_, ir)) = compile_scalar_default_typed(pyql, schema) else {
continue;
};
if crate::ir::default_blocker(&ir).is_some() {
continue;
}
let Some(actual) = infer_ir_type(&ir) else { continue };
if !types_compatible(actual, "uuid") {
errors.push(mismatch(
context.clone(),
format!(
"default value type mismatch for '{}': expected uuid, default produces {}",
context, actual
),
));
}
}
if !td.abstract_ && !td.junction {
for (pointer, pyql) in crate::ir::inlined_pointer_defaults(td, schema) {
let context = format!("{type_name}.{pointer} (default)");
let (column, ir) = match crate::ir::compile_inlined_default(&type_name, &pointer, &pyql, schema) {
Ok(assignment) => assignment,
Err(e) => {
errors.push(mismatch(context.clone(), format!("default for '{context}': {e}")));
continue;
}
};
let Some(actual) = infer_ir_type(&ir) else { continue };
let declared = td
.properties
.iter()
.find(|p| p.name == column)
.map(|p| p.pg_type.as_str())
.unwrap_or("uuid");
if !types_compatible(actual, declared) {
errors.push(mismatch(
context.clone(),
format!(
"default value type mismatch for '{context}': expected {declared}, \
default produces {actual}"
),
));
}
}
}
if !td.abstract_ && !td.junction {
for on in [1u8, 2] {
let assignments = match crate::ir::compile_rewrite_assignments(&type_name, on, schema) {
Ok(assignments) => assignments,
Err(e) => {
let context = format!("{type_name} (rewrite)");
errors.push(mismatch(context.clone(), format!("{context}: {e}")));
continue;
}
};
for assignment in assignments {
let pg_type = td
.properties
.iter()
.find(|p| p.name == assignment.pointer)
.map(|p| p.pg_type.as_str())
.unwrap_or("uuid");
let Some(actual) = infer_ir_type(&assignment.ir) else {
continue;
};
if !types_compatible(actual, pg_type) {
let context = format!("{}.{} (rewrite)", type_name, assignment.pointer);
errors.push(mismatch(
context.clone(),
format!(
"rewrite handler type mismatch for '{}': expected {}, handler produces {}",
context, pg_type, actual
),
));
}
}
}
}
for trig in &td.triggers {
if let Err(e) = compile_trigger_handler(&trig.handler, &type_name, trig.on, schema) {
let context = format!("{type_name} (trigger)");
let handler: String = trig.handler.chars().take(80).collect();
errors.push(mismatch(context.clone(), format!("{context} `{handler}…`: {e}")));
}
}
}
for alias in &schema.aliases {
let parsed = match crate::parse::parse(&alias.expr) {
Ok(ast) => ast,
Err(e) => {
errors.push(PyQLError::Syntax(e));
continue;
}
};
if let Err(e) = compile(&parsed, schema) {
errors.push(e);
}
}
for global in &schema.globals {
let Some(computed_expr) = &global.computed_expr else {
continue;
};
let parsed = match crate::parse::parse(computed_expr) {
Ok(ast) => ast,
Err(e) => {
errors.push(PyQLError::Syntax(e));
continue;
}
};
if let Err(e) = compile(&parsed, schema) {
errors.push(e);
}
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{
AliasDescriptor, ComputedDescriptor, FunctionDescriptor, FunctionParamDescriptor, GlobalDescriptor,
LinkDescriptor, PropertyDescriptor, RewriteEntry, TriggerDescriptor, TypeDescriptor,
};
fn ts_prop(name: &str, nullable: bool) -> PropertyDescriptor {
PropertyDescriptor {
name: name.into(),
pg_type: "timestamptz".into(),
nullable,
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 partitioned(part: crate::schema::PartitionDescriptor, props: Vec<PropertyDescriptor>) -> SchemaDescriptor {
let mut td = person_type(vec![], props);
td.partition = Some(part);
SchemaDescriptor {
types: vec![td],
..Default::default()
}
}
fn monthly(pointer: &str) -> crate::schema::PartitionDescriptor {
crate::schema::PartitionDescriptor {
pointer: pointer.into(),
interval: crate::schema::PartitionInterval::Monthly,
premake: 4,
retention: None,
}
}
fn only_error(schema: &SchemaDescriptor) -> String {
let errors = validate_partitions(schema);
assert_eq!(errors.len(), 1, "expected exactly one error, got {errors:#?}");
errors[0].to_string()
}
#[test]
fn a_valid_partition_passes() {
let schema = partitioned(monthly("occurred_at"), vec![ts_prop("occurred_at", false)]);
assert!(validate_partitions(&schema).is_empty());
}
#[test]
fn a_partition_on_an_abstract_type_is_rejected() {
let mut schema = partitioned(monthly("occurred_at"), vec![ts_prop("occurred_at", false)]);
schema.types[0].abstract_ = true;
schema.types[0].materialized = false;
assert!(only_error(&schema).contains("abstract"));
}
#[test]
fn a_partition_on_an_interface_is_rejected() {
let mut schema = partitioned(monthly("occurred_at"), vec![ts_prop("occurred_at", false)]);
schema.types[0].abstract_ = true;
schema.types[0].materialized = true;
assert!(only_error(&schema).contains("interface"));
}
#[test]
fn a_partition_on_an_unknown_pointer_is_rejected() {
let schema = partitioned(monthly("nope"), vec![ts_prop("occurred_at", false)]);
assert!(only_error(&schema).contains("not a property"));
}
#[test]
fn a_partition_on_a_non_temporal_property_is_rejected() {
let mut prop = ts_prop("occurred_at", false);
prop.pg_type = "text".into();
let schema = partitioned(monthly("occurred_at"), vec![prop]);
assert!(only_error(&schema).contains("datetime or date"));
}
#[test]
fn a_partition_on_an_optional_property_is_rejected() {
let schema = partitioned(monthly("occurred_at"), vec![ts_prop("occurred_at", true)]);
assert!(only_error(&schema).contains("can never be empty"));
}
#[test]
fn premake_zero_is_rejected() {
let mut part = monthly("occurred_at");
part.premake = 0;
let schema = partitioned(part, vec![ts_prop("occurred_at", false)]);
assert!(only_error(&schema).contains("premake=0"));
}
#[test]
fn date_and_naive_timestamp_keys_are_accepted() {
for pg_type in ["date", "timestamp"] {
let mut prop = ts_prop("occurred_at", false);
prop.pg_type = pg_type.into();
let schema = partitioned(monthly("occurred_at"), vec![prop]);
assert!(
validate_partitions(&schema).is_empty(),
"{pg_type} should be a valid partition key"
);
}
}
#[test]
fn retention_renders_as_a_postgres_interval() {
use crate::schema::{PartitionDescriptor, PartitionInterval};
let with = |interval, retention| {
PartitionDescriptor {
pointer: "t".into(),
interval,
premake: 4,
retention: Some(retention),
}
.retention_interval()
};
assert_eq!(with(PartitionInterval::Daily, 30), Some("30 days".to_string()));
assert_eq!(with(PartitionInterval::Monthly, 12), Some("12 months".to_string()));
assert_eq!(with(PartitionInterval::Yearly, 7), Some("7 years".to_string()));
assert_eq!(monthly("t").retention_interval(), None);
}
fn person_type(computed: Vec<ComputedDescriptor>, properties: Vec<PropertyDescriptor>) -> TypeDescriptor {
let mut props = 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: false,
rewrites: vec![],
tuple_members: None,
column_type: None,
}];
props.extend(properties);
TypeDescriptor {
name: "Person".into(),
module: "default".into(),
table: "default_person".into(),
abstract_: false,
materialized: true,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: props,
links: vec![],
multilinks: vec![],
computed,
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
}
}
fn base_property(name: &str, pg_type: &str) -> PropertyDescriptor {
PropertyDescriptor {
name: name.into(),
pg_type: pg_type.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 minimal_schema(types: Vec<TypeDescriptor>, functions: Vec<FunctionDescriptor>) -> SchemaDescriptor {
SchemaDescriptor {
types,
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions,
aliases: vec![],
channels: vec![],
..Default::default()
}
}
#[test]
fn function_return_type_match_passes() {
let fd = FunctionDescriptor {
name: "myid".into(),
module: "default".into(),
params: vec![FunctionParamDescriptor {
name: "a".into(),
pg_type: "int8".into(),
}],
return_pg_type: "int8".into(),
body: "a".into(),
return_is_object: false,
return_is_set: false,
return_is_polymorphic: false,
volatility: "immutable".into(),
};
let schema = minimal_schema(vec![], vec![fd]);
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn function_return_type_mismatch_rejected() {
let fd = FunctionDescriptor {
name: "bad".into(),
module: "default".into(),
params: vec![FunctionParamDescriptor {
name: "a".into(),
pg_type: "text".into(),
}],
return_pg_type: "int8".into(),
body: "a".into(),
return_is_object: false,
return_is_set: false,
return_is_polymorphic: false,
volatility: "immutable".into(),
};
let schema = minimal_schema(vec![], vec![fd]);
let errs = validate_schema_types(&schema).unwrap_err();
assert_eq!(errs.len(), 1);
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("bad"), "{msg}");
assert!(msg.contains("declared int8"), "{msg}");
assert!(msg.contains("produces text"), "{msg}");
}
#[test]
fn function_call_body_return_type_is_checked() {
let fd = FunctionDescriptor {
name: "caller".into(),
module: "default".into(),
params: vec![],
return_pg_type: "int8".into(),
body: "str_lower('X')".into(),
return_is_object: false,
return_is_set: false,
return_is_polymorphic: false,
volatility: "immutable".into(),
};
let schema = minimal_schema(vec![], vec![fd]);
let errs = validate_schema_types(&schema).unwrap_err();
let msg = errs[0].to_string();
assert!(msg.contains("declared int8"), "{msg}");
assert!(msg.contains("produces text"), "{msg}");
}
#[test]
fn a_function_body_calling_a_user_function_checks_its_return_type() {
let callee = FunctionDescriptor {
name: "gives_text".into(),
module: "default".into(),
params: vec![],
return_pg_type: "text".into(),
body: "'x'".into(),
return_is_object: false,
return_is_set: false,
return_is_polymorphic: false,
volatility: "immutable".into(),
};
let caller = FunctionDescriptor {
name: "caller".into(),
module: "default".into(),
params: vec![],
return_pg_type: "int8".into(),
body: "default::gives_text()".into(),
return_is_object: false,
return_is_set: false,
return_is_polymorphic: false,
volatility: "immutable".into(),
};
let schema = minimal_schema(vec![], vec![callee, caller]);
let errs = validate_schema_types(&schema).unwrap_err();
let msg = errs[0].to_string();
assert!(msg.contains("declared int8"), "{msg}");
assert!(msg.contains("produces text"), "{msg}");
}
#[test]
fn computed_return_type_match_passes() {
let cd = ComputedDescriptor {
name: "double_id".into(),
expression: ".id".into(),
return_type: Some("uuid".into()),
link_target: None,
link_multi: false,
};
let td = person_type(vec![cd], vec![]);
let schema = minimal_schema(vec![td], vec![]);
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn computed_return_type_mismatch_rejected() {
let cd = ComputedDescriptor {
name: "bad".into(),
expression: ".id".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
};
let td = person_type(vec![cd], vec![]);
let schema = minimal_schema(vec![td], vec![]);
let errs = validate_schema_types(&schema).unwrap_err();
assert_eq!(errs.len(), 1);
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("Person.bad"), "{msg}");
}
#[test]
fn default_type_match_passes() {
let mut prop = base_property("score", "int8");
prop.default_pyql = Some("1".into());
let td = person_type(vec![], vec![prop]);
let schema = minimal_schema(vec![td], vec![]);
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn default_type_mismatch_rejected() {
let mut prop = base_property("score", "int8");
prop.default_pyql = Some("'not a number'".into());
let td = person_type(vec![], vec![prop]);
let schema = minimal_schema(vec![td], vec![]);
let errs = validate_schema_types(&schema).unwrap_err();
assert_eq!(errs.len(), 1);
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("Person.score"), "{msg}");
}
#[test]
fn a_default_naming_a_function_that_does_not_exist_is_rejected() {
let mut prop = base_property("token", "uuid");
prop.default_pyql = Some("std::uuid_generate_v7j()".into());
let td = person_type(vec![], vec![prop]);
let schema = minimal_schema(vec![td], vec![]);
let errs = validate_schema_types(&schema).unwrap_err();
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("Person.token"), "{msg}");
assert!(msg.contains("does not exist"), "{msg}");
}
#[test]
fn a_default_calling_a_real_function_wrongly_is_rejected() {
let mut prop = base_property("name", "text");
prop.default_pyql = Some("std::str_lower('A', 'B')".into());
let td = person_type(vec![], vec![prop]);
let schema = minimal_schema(vec![td], vec![]);
let errs = validate_schema_types(&schema).unwrap_err();
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("Person.name"), "{msg}");
assert!(msg.contains("takes 1 argument(s), got 2"), "{msg}");
}
#[test]
fn a_default_that_is_a_valid_stdlib_call_still_passes() {
let mut prop = base_property("token", "uuid");
prop.default_pyql = Some("std::uuid_generate_v7()".into());
let td = person_type(vec![], vec![prop]);
let schema = minimal_schema(vec![td], vec![]);
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn a_computed_calling_a_set_returning_function_declared_single_is_rejected() {
let fd = FunctionDescriptor {
name: "gives_many".into(),
module: "default".into(),
params: vec![],
return_pg_type: "int8".into(),
body: "{1, 2}".into(),
return_is_object: false,
return_is_set: true,
return_is_polymorphic: false,
volatility: "immutable".into(),
};
let cd = ComputedDescriptor {
name: "n".into(),
expression: "default::gives_many()".into(),
return_type: Some("int8".into()),
link_target: None,
link_multi: false,
};
let td = person_type(vec![cd], vec![]);
let schema = minimal_schema(vec![td], vec![fd]);
let errs = validate_schema_types(&schema).unwrap_err();
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("cardinality mismatch"), "{msg}");
assert!(msg.contains("gives_many"), "{msg}");
}
#[test]
fn a_computed_crossing_a_multilink_declared_single_is_rejected() {
let cd = ComputedDescriptor {
name: "friend_names".into(),
expression: ".friends.name".into(),
return_type: Some("text".into()),
link_target: None,
link_multi: false,
};
let mut td = person_type(vec![cd], vec![base_property("name", "text")]);
td.multilinks = vec![crate::schema::MultiLinkDescriptor {
name: "friends".into(),
target: "default::Person".into(),
through: None,
nullable: true,
description: None,
default_pyql: None,
on_delete: vec![],
is_exclusive: false,
}];
let schema = minimal_schema(vec![td], vec![]);
let errs = validate_schema_types(&schema).unwrap_err();
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("cardinality mismatch"), "{msg}");
assert!(msg.contains("multilink"), "{msg}");
}
#[test]
fn the_same_computed_declared_as_an_array_passes() {
let cd = ComputedDescriptor {
name: "friend_names".into(),
expression: ".friends.name".into(),
return_type: Some("text[]".into()),
link_target: None,
link_multi: false,
};
let mut td = person_type(vec![cd], vec![base_property("name", "text")]);
td.multilinks = vec![crate::schema::MultiLinkDescriptor {
name: "friends".into(),
target: "default::Person".into(),
through: None,
nullable: true,
description: None,
default_pyql: None,
on_delete: vec![],
is_exclusive: false,
}];
let schema = minimal_schema(vec![td], vec![]);
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn link_default_selecting_an_object_moves_into_the_insert() {
let mut td = person_type(vec![], vec![base_property("name", "text")]);
td.links = vec![LinkDescriptor {
name: "manager".into(),
target: "default::Person".into(),
nullable: true,
through: None,
description: None,
default_pyql: Some("(select Person filter .name = 'boss' limit 1)".into()),
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
}];
let schema = minimal_schema(vec![td], vec![]);
validate_schema_types(&schema).expect("an inlined default is not an error");
let inlined = crate::ir::inlined_pointer_defaults(&schema.types[0], &schema);
assert_eq!(inlined.iter().map(|(p, _)| p.as_str()).collect::<Vec<_>>(), ["manager"]);
}
#[test]
fn link_default_that_is_a_constant_passes() {
let mut td = person_type(vec![], vec![base_property("name", "text")]);
td.links = vec![LinkDescriptor {
name: "manager".into(),
target: "default::Person".into(),
nullable: true,
through: None,
description: None,
default_pyql: Some("<uuid>'00000000-0000-0000-0000-000000000000'".into()),
is_exclusive: false,
is_readonly: false,
rewrites: vec![],
on_delete: vec![],
}];
let schema = minimal_schema(vec![td], vec![]);
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn rewrite_type_match_passes() {
let mut prop = base_property("name", "text");
prop.rewrites = vec![RewriteEntry {
on: 1,
handler: "'unnamed'".into(),
}];
let td = person_type(vec![], vec![prop]);
let schema = minimal_schema(vec![td], vec![]);
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn rewrite_type_mismatch_rejected() {
let mut prop = base_property("name", "text");
prop.rewrites = vec![RewriteEntry {
on: 1,
handler: "1".into(),
}];
let td = person_type(vec![], vec![prop]);
let schema = minimal_schema(vec![td], vec![]);
let errs = validate_schema_types(&schema).unwrap_err();
assert_eq!(errs.len(), 1);
let (_, msg, _) = errs[0].class_name_message_position();
assert!(msg.contains("Person.name (rewrite)"), "{msg}");
assert!(msg.contains("expected text"), "{msg}");
}
#[test]
fn trigger_handler_compiles_passes() {
let mut td = person_type(vec![], vec![]);
td.triggers = vec![TriggerDescriptor {
on: 1,
timing: "After".into(),
handler: "select Person".into(),
}];
let schema = minimal_schema(vec![td], vec![]);
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn trigger_handler_unknown_field_rejected() {
let mut td = person_type(vec![], vec![]);
td.triggers = vec![TriggerDescriptor {
on: 1,
timing: "After".into(),
handler: "select Person filter .nonexistent_field = 1".into(),
}];
let schema = minimal_schema(vec![td], vec![]);
let errs = validate_schema_types(&schema).unwrap_err();
assert_eq!(errs.len(), 1);
}
#[test]
fn alias_compiles_passes() {
let td = person_type(vec![], vec![]);
let mut schema = minimal_schema(vec![td], vec![]);
schema.aliases = vec![AliasDescriptor {
name: "all_people".into(),
module: "default".into(),
expr: "select Person".into(),
}];
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn alias_unknown_type_rejected() {
let mut schema = minimal_schema(vec![], vec![]);
schema.aliases = vec![AliasDescriptor {
name: "bad".into(),
module: "default".into(),
expr: "select NoSuchType".into(),
}];
let errs = validate_schema_types(&schema).unwrap_err();
assert_eq!(errs.len(), 1);
}
fn base_global(name: &str) -> GlobalDescriptor {
GlobalDescriptor {
name: name.into(),
module: "default".into(),
scalar_type: "std::str".into(),
required: false,
default_expr: None,
computed_expr: None,
}
}
#[test]
fn computed_global_compiles_passes() {
let td = person_type(vec![], vec![]);
let mut schema = minimal_schema(vec![td], vec![]);
let mut g = base_global("first_person");
g.computed_expr = Some("select Person".into());
schema.globals = vec![g];
assert!(validate_schema_types(&schema).is_ok());
}
#[test]
fn computed_global_unknown_type_rejected() {
let mut schema = minimal_schema(vec![], vec![]);
let mut g = base_global("bad");
g.computed_expr = Some("select NoSuchType".into());
schema.globals = vec![g];
let errs = validate_schema_types(&schema).unwrap_err();
assert_eq!(errs.len(), 1);
}
#[test]
fn session_global_with_no_computed_expr_is_untouched() {
let mut schema = minimal_schema(vec![], vec![]);
let mut g = base_global("current_user_id");
g.default_expr = Some("'not actually pyql, just a sql literal'".into());
schema.globals = vec![g];
assert!(validate_schema_types(&schema).is_ok());
}
}