Skip to content

Elo Validation Language

Elo is a domain-specific expression language designed for safe, human-readable validation logic. FraiseQL integrates Elo to enable powerful custom scalar validation at runtime.

Elo is an expression language created by Bernard Lambeau for expressing business rules and validation constraints. FraiseQL uses Elo to:

  • Validate at runtime: Elo expressions are interpreted when input values are received
  • Execute safely: No arbitrary code execution, no injection vulnerabilities
  • Perform efficiently: Validation executes in <100µs (99th percentile) per check
  • Read naturally: Concise, expressive syntax for complex validation rules

In FraiseQL, Elo expressions let you define custom scalar validation rules that are:

  • Type-safe
  • Database-agnostic
  • Testable independently
  • Reusable across your schema

Comparison operators:

value < 100
value <= 100
value > 0
value >= 0
value == "admin"
value != "pending"

Logical operators:

value > 0 && value < 100 # AND
status == "active" || status == "pending" # OR
!is_deleted # NOT

String patterns:

matches(value, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
matches(value, "^[0-9]{3}-[0-9]{2}-[0-9]{4}$") # SSN format

Elo provides built-in functions for common validation tasks:

String functions:

length(value) >= 3 # String length
length(value) <= 255

Type checking:

Type checking is handled by the base type declared on the scalar — FraiseQL validates the type at the schema level before evaluating Elo expressions. The is_string(), is_number(), and is_date() functions are not implemented in the Elo evaluator.

Date functions:

today() # Current date
now() # Current timestamp

Age function:

age(value) >= 18 # Person must be at least 18 years old
age(value) < 65 # Person must be under 65

Numeric functions:

value % 2 == 0 # Even number
value % 10 == value % 11 # Custom math

Access different parts of your data:

# Direct value reference
value >= 0
# Nested object access (in context of mutations)
user.email
customer.billing_address.zip_code

Numbers:

value > 100
value <= 1000.50

Strings:

status == "active"
country == "US"

Booleans:

is_verified == true
is_deleted == false

Dates:

created_at >= 2024-01-01
birth_date < 2006-01-01 # Person must be 18+

FraiseQL manages custom scalars through a thread-safe CustomTypeRegistry that stores CustomTypeDef definitions. When you define a custom scalar, FraiseQL compiles it into a schema definition that includes the Elo expression.

Custom scalars must subclass CustomScalar and implement serialize, parse_value, and parse_literal. Elo validation is attached to the scalar’s CustomTypeDef in the compiled schema — it is not a class attribute in Python.

from fraiseql.scalars import CustomScalar
import fraiseql
import re
@fraiseql.scalar
class Email(CustomScalar):
name = "Email"
def serialize(self, value: str) -> str:
return str(value).lower()
def parse_value(self, value: str) -> str:
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', str(value)):
raise ValueError(f"Invalid email address: {value}")
return str(value).lower()
def parse_literal(self, ast) -> str:
if hasattr(ast, 'value'):
return self.parse_value(ast.value)
raise ValueError("Invalid email literal")

The Elo expression for runtime validation is declared in the compiled schema — the Python class defines the serialization logic, while the Elo expression provides the validation rule.

When you define this scalar, FraiseQL creates a CustomTypeDef:

pub struct CustomTypeDef {
pub name: "Email",
pub description: Some("Valid RFC 5322 email address"),
pub specified_by_url: None,
pub validation_rules: [], // Built-in validators (if specified)
pub elo_expression: Some('matches(value, ...)'),
pub base_type: Some("String"),
}

After compilation, custom scalars appear in schema.compiled.json:

{
"custom_types": [
{
"name": "Email",
"description": "Valid RFC 5322 email address",
"base_type": "String",
"elo_expression": "matches(value, \"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$\") && length(value) <= 254",
"specified_by_url": null
},
{
"name": "ISBN",
"description": "International Standard Book Number",
"base_type": "String",
"elo_expression": "(length(value) == 10 && matches(value, \"^[0-9X]{10}$\")) || (length(value) == 13 && matches(value, \"^978[0-9]{10}$|^979[0-9]{10}$\"))"
}
]
}

When a query or mutation provides a value of a custom scalar type, FraiseQL’s EloExpressionEvaluator interprets the Elo expression at runtime:

  1. Type Lookup: Retrieve CustomTypeDef from registry
  2. Rule Execution: Execute any built-in validation rules first
  3. Elo Evaluation: If elo_expression is present, interpret it with the input value
  4. Result: Return success or validation error

Example flow:

Input Value: "john@example.com"
CustomTypeRegistry.get("Email")
Execute elo_expression:
matches(value, "^[a-zA-Z0-9._%+-]+@...") && length(value) <= 254
Result: Valid
  • Compile time (schema building): Elo expressions parsed and validated once
  • Runtime (per request): Expressions interpreted in <100µs per validation
  • Memory: Minimal (evaluator is stateless)

Combine multiple conditions with logical operators in the Elo expression:

length(value) >= 8 &&
matches(value, "[A-Z]") &&
matches(value, "[a-z]") &&
matches(value, "[0-9]") &&
matches(value, "[!@#$%^&*]")

This expression is evaluated left-to-right with short-circuit AND logic — if length(value) >= 8 fails, the remaining conditions aren’t checked.

Here’s a complete example showing how a custom ISBN scalar is defined, compiled, and validated:

import fraiseql
from fraiseql.scalars import CustomScalar, ID
import re
@fraiseql.scalar
class ISBN(CustomScalar):
name = "ISBN"
def serialize(self, value: str) -> str:
return str(value)
def parse_value(self, value: str) -> str:
v = str(value).replace("-", "")
if not re.match(r'^([0-9X]{10}|[0-9]{13})$', v):
raise ValueError(f"Invalid ISBN: {value}")
return str(value)
def parse_literal(self, ast) -> str:
if hasattr(ast, 'value'):
return self.parse_value(ast.value)
raise ValueError("Invalid ISBN literal")
@fraiseql.type
class Book:
id: ID
title: str
isbn: ISBN
author: str
@fraiseql.query
def book_by_isbn(isbn: ISBN) -> Book | None:
return fraiseql.config(sql_source="v_book_by_isbn")
Terminal window
$ fraiseql compile schema.json -o schema.compiled.json
Custom type 'ISBN' registered
Elo expression validated: (length(value) == 10...) || (length(value) == 13...)
1 custom type compiled
{
"custom_types": [
{
"name": "ISBN",
"description": "ISBN-10 or ISBN-13 format",
"base_type": "String",
"elo_expression": "(length(value) == 10 && matches(value, /^[0-9X]{10}$/)) || (length(value) == 13 && matches(value, /^978[0-9]{10}$|^979[0-9]{10}$/))",
"specified_by_url": null
}
],
"types": {
"Book": {
"fields": {
"isbn": { "type": "ISBN", "required": true }
}
}
}
}

When a client queries:

query {
bookByIsbn(isbn: "978-0-13-468599-1") {
title
author
}
}

FraiseQL’s CustomTypeRegistry:

  1. Looks up ISBN custom type definition
  2. Executes the Elo expression: (length("978-0-13-468599-1") == 10 ...) || ...
  3. Result: length(...) == 13true, validates successfully

If the client provides an invalid ISBN:

query {
bookByIsbn(isbn: "not-an-isbn") {
title
}
}

The validator:

  1. Evaluates: (length("not-an-isbn") == 10 ...) || ...
  2. Result: Both conditions fail → false
  3. Returns error: Validation error: 'not-an-isbn' is not a valid ISBN

The Elo expressions below are used in the elo_expression field of the compiled CustomTypeDef. Each scalar is implemented as a CustomScalar subclass in Python (see the ISBN example above). The Elo expression shown is the validation rule embedded in the compiled schema.

matches(value, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
(length(value) == 10 && matches(value, "^[0-9X]{10}$")) ||
(length(value) == 13 && matches(value, "^978[0-9]{10}$|^979[0-9]{10}$"))
matches(value, "^https://[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
length(value) >= 3 &&
length(value) <= 20 &&
matches(value, "^[a-zA-Z_][a-zA-Z0-9_]*$")
value >= 0.01 && value <= 999999.99
matches(value, "^[0-9]{3}-[0-9]{3}-[0-9]{4}$|^[0-9]{10}$")

In mutation contexts, reference multiple fields from the validation context:

start_date < end_date

The Elo evaluator resolves field names via dot-notation lookup in the JSON context passed at validation time.

The today() and now() functions are implemented and return the current date/timestamp:

value > today() # Date must be in the future

When FraiseQL compiles your schema, it:

  1. Parses each Elo expression and validates the syntax
  2. Type-checks against the scalar’s base type
  3. Validates that all functions referenced are implemented in the evaluator
  4. Stores the expression in schema.compiled.json for runtime use

FraiseQL can also generate PostgreSQL CHECK constraints and JavaScript validation functions from Elo expressions at compile time:

  • SQL CHECK constraints: fraiseql compile --emit-checks generates ALTER TABLE ... ADD CONSTRAINT statements for tb_* tables, enforcing Elo rules at the database level.
  • JavaScript codegen: fraiseql compile --emit-js generates a JavaScript module exporting a validation function per custom scalar, suitable for client-side pre-validation.

Error: “Unknown function ‘validate‘“

# Wrong
validate(value)
# Correct — use built-in functions: matches(), contains(), length(), today(), now()
length(value) > 0

Error: “Cannot compare string to number”

# Wrong - comparing int to string
value == "18"
# Correct - comparing int to int
value == 18

Patterns in matches() must be quoted strings, not regex literals:

# Wrong - slash-delimited regex literals are not supported
matches(value, /^[a-z]+@[a-z]+.[a-z]+$/)
# Correct - use quoted string, escape backslashes
matches(value, "^[a-z]+@[a-z]+\\.[a-z]+$")

Optimization tips:

  • Put most restrictive checks first (short-circuit AND)
  • Add CHECK constraints to your tb_* tables for high-volume database-level enforcement
  • Batch validate in bulk operations

Validate expressions using the fraiseql compile CLI — it reports syntax errors and unknown function references at compile time. The fraiseql.validation and fraiseql.elo Python modules do not exist; expression testing is done through the CLI or by running fraiseql compile on your schema.

Terminal window
# Compile validates all Elo expressions and reports errors
fraiseql compile
# Error: unknown function 'is_string' in elo_expression for scalar 'Email' at line 5