use std::sync::OnceLock;
pub mod ddl;
mod registry;
pub use ddl::export_stdlib;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PylonType {
Str,
Bool,
Int16,
Int32,
Int64,
Float32,
Float64,
Decimal,
BigInt,
Uuid,
Json,
Bytes,
Datetime,
Duration,
LocalDatetime,
LocalDate,
LocalTime,
RelativeDuration,
DateDuration,
Vector,
Geometry,
Geography,
Box2D,
Box3D,
Any,
AnyOrderable,
AnyPoint,
Array(Box<PylonType>),
Set(Box<PylonType>),
Optional(Box<PylonType>),
Range(Box<PylonType>),
Multirange(Box<PylonType>),
Tuple(Vec<PylonType>),
}
impl PylonType {
pub fn scalar_pg_type(&self) -> Option<&'static str> {
use PylonType::*;
Some(match self {
Str => "text",
Bool => "boolean",
Int16 => "int2",
Int32 => "int4",
Int64 => "int8",
Float32 => "float4",
Float64 => "float8",
Decimal | BigInt => "numeric",
Uuid => "uuid",
Json => "jsonb",
Bytes => "bytea",
Datetime => "timestamptz",
Duration | RelativeDuration | DateDuration => "interval",
LocalDatetime => "timestamp",
LocalDate => "date",
LocalTime => "time",
_ => return None,
})
}
pub fn pyql_name(&self) -> String {
use PylonType::*;
match self {
Str => "str".into(),
Bool => "bool".into(),
Int16 => "int16".into(),
Int32 => "int32".into(),
Int64 => "int64".into(),
Float32 => "float32".into(),
Float64 => "float64".into(),
Decimal => "decimal".into(),
BigInt => "bigint".into(),
Uuid => "uuid".into(),
Json => "json".into(),
Bytes => "bytes".into(),
Datetime => "datetime".into(),
Duration => "duration".into(),
LocalDatetime => "cal::local_datetime".into(),
LocalDate => "cal::local_date".into(),
LocalTime => "cal::local_time".into(),
RelativeDuration => "cal::relative_duration".into(),
DateDuration => "cal::date_duration".into(),
Vector => "pgvector::vector".into(),
Geometry => "postgis::geometry".into(),
Geography => "postgis::geography".into(),
Box2D => "postgis::box2d".into(),
Box3D => "postgis::box3d".into(),
Any => "any".into(),
AnyOrderable => "anyorderable".into(),
AnyPoint => "anypoint".into(),
Array(inner) => format!("array<{}>", inner.pyql_name()),
Set(inner) => format!("set<{}>", inner.pyql_name()),
Optional(inner) => format!("optional<{}>", inner.pyql_name()),
Range(inner) => format!("range<{}>", inner.pyql_name()),
Multirange(inner) => format!("multirange<{}>", inner.pyql_name()),
Tuple(ts) => format!(
"tuple<{}>",
ts.iter().map(|t| t.pyql_name()).collect::<Vec<_>>().join(", ")
),
}
}
pub fn is_set(&self) -> bool {
matches!(self, PylonType::Set(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SqlLanguage {
Sql,
PlPgSql,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FnVolatility {
Immutable,
Stable,
Volatile,
Modifying,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PylonFnDef {
pub name: &'static str,
pub language: SqlLanguage,
pub volatility: FnVolatility,
pub strict: bool,
pub returns_override: Option<&'static str>,
pub body: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImplStrategy {
SqlBuiltin(&'static str),
SqlExpression(&'static str),
SqlOperator(&'static str),
PylonFunction(PylonFnDef),
TranspilerIntrinsic(&'static str),
}
#[derive(Debug, Clone)]
pub struct Param {
pub name: &'static str,
pub ty: PylonType,
pub variadic: bool,
pub named_only: Option<NamedDefault>,
pub keyword: Option<&'static str>,
}
impl Param {
pub fn keyword(&self) -> &'static str {
self.keyword.unwrap_or(self.name)
}
}
#[derive(Debug, Clone, Copy)]
pub enum NamedDefault {
Int(i64),
Bool(bool),
Str(&'static str),
Required,
Empty,
}
#[derive(Debug, Clone)]
pub struct FnDescriptor {
pub namespace: &'static str,
pub name: &'static str,
pub params: Vec<Param>,
pub return_type: PylonType,
pub impl_strategy: ImplStrategy,
pub cast_target: bool,
pub volatility: FnVolatility,
}
static STDLIB: OnceLock<Vec<FnDescriptor>> = OnceLock::new();
pub fn registry() -> &'static [FnDescriptor] {
STDLIB.get_or_init(registry::build)
}
impl FnVolatility {
pub fn as_str(&self) -> &'static str {
match self {
FnVolatility::Immutable => "immutable",
FnVolatility::Stable => "stable",
FnVolatility::Volatile => "volatile",
FnVolatility::Modifying => "modifying",
}
}
}
impl FnDescriptor {
pub fn is_aggregate(&self) -> bool {
self.params.iter().any(|p| p.ty.is_set())
}
pub fn returns_set(&self) -> bool {
self.return_type.is_set()
}
pub fn is_intrinsic(&self) -> bool {
matches!(self.impl_strategy, ImplStrategy::TranspilerIntrinsic(_))
}
pub fn is_variadic(&self) -> bool {
self.params
.iter()
.rev()
.find(|p| p.named_only.is_none())
.is_some_and(|p| p.variadic)
}
pub fn variadic_index(&self) -> Option<usize> {
self.params.iter().position(|p| p.variadic)
}
pub fn named_count(&self) -> usize {
self.params.iter().filter(|p| p.named_only.is_some()).count()
}
}
pub fn lookup(namespace: &str, name: &str) -> Vec<&'static FnDescriptor> {
registry()
.iter()
.filter(|f| f.namespace == namespace && f.name == name)
.collect()
}
static STDLIB_ENUMS: OnceLock<Vec<crate::schema::EnumDescriptor>> = OnceLock::new();
pub fn stdlib_enums() -> &'static [crate::schema::EnumDescriptor] {
STDLIB_ENUMS.get_or_init(|| {
vec![
crate::schema::EnumDescriptor {
name: "Endian".to_string(),
module: "std".to_string(),
members: vec!["Little".to_string(), "Big".to_string()],
},
crate::schema::EnumDescriptor {
name: "JsonEmpty".to_string(),
module: "std".to_string(),
members: ["ReturnEmpty", "ReturnTarget", "Error", "UseNull", "DeleteKey"]
.map(str::to_string)
.to_vec(),
},
crate::schema::EnumDescriptor {
name: "Base64Alphabet".to_string(),
module: "enc".to_string(),
members: vec!["standard".to_string(), "urlsafe".to_string()],
},
]
})
}
pub fn lookup_enum(name: &str) -> Option<&'static crate::schema::EnumDescriptor> {
stdlib_enums()
.iter()
.find(|e| e.name == name || format!("{}::{}", e.module, e.name) == name)
}
pub fn cast_targets() -> impl Iterator<Item = &'static FnDescriptor> {
registry().iter().filter(|f| f.cast_target)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn volatility_is_consistent_across_overloads() {
let mut seen: HashMap<(&str, &str), FnVolatility> = HashMap::new();
for d in registry() {
let key = (d.namespace, d.name);
match seen.get(&key) {
Some(existing) => assert_eq!(
*existing, d.volatility,
"{}::{} declares more than one volatility across its overloads ({:?} vs {:?}) \
— every overload of a name must agree",
d.namespace, d.name, existing, d.volatility,
),
None => {
seen.insert(key, d.volatility);
}
}
}
}
#[test]
fn descriptor_volatility_matches_pylon_fn_def() {
for d in registry() {
if let ImplStrategy::PylonFunction(def) = &d.impl_strategy {
assert_eq!(
def.volatility, d.volatility,
"{}::{} declares {:?} on its PylonFnDef but {:?} on its descriptor",
d.namespace, d.name, def.volatility, d.volatility,
);
}
}
}
}