Skip to main content

pylon_core/stdlib/
mod.rs

1//
2// This source file is part of the Pylon open source project.
3//
4// Copyright (c) 2026 Jaldis B.V.
5//
6// Licensed under the MIT OR Apache-2.0 license (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     https://opensource.org/licenses/MIT
11//     https://www.apache.org/licenses/LICENSE-2.0
12//
13// Unless required by applicable law or agreed to in writing, software
14// distributed under the License is distributed on an "AS IS" BASIS,
15// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16// See the License for the specific language governing permissions and
17// limitations under the License.
18//
19
20use std::sync::OnceLock;
21
22pub mod ddl;
23mod registry;
24pub use ddl::export_stdlib;
25
26// ── Type system ──────────────────────────────────────────────────────────────
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum PylonType {
30    // Scalar primitives
31    Str,
32    Bool,
33    Int16,
34    Int32,
35    Int64,
36    Float32,
37    Float64,
38    Decimal,
39    BigInt,
40    Uuid,
41    Json,
42    Bytes,
43    // Temporal
44    Datetime,
45    Duration,
46    // cal:: types
47    LocalDatetime,
48    LocalDate,
49    LocalTime,
50    RelativeDuration,
51    /// Months and days only — the calendar-relative duration that a date
52    /// arithmetic yields. `interval` like the other two, and so not told
53    /// apart from them by a value's PostgreSQL type; what it names is the
54    /// part of the stdlib that is about dates rather than about clocks.
55    DateDuration,
56    // pgvector:: types
57    Vector,
58    // postgis:: types
59    Geometry,
60    Geography,
61    Box2D,
62    Box3D,
63    // Polymorphic
64    Any,
65    AnyOrderable,
66    AnyPoint,
67    // Composite
68    Array(Box<PylonType>),
69    Set(Box<PylonType>),
70    Optional(Box<PylonType>),
71    Range(Box<PylonType>),
72    Multirange(Box<PylonType>),
73    Tuple(Vec<PylonType>),
74}
75
76impl PylonType {
77    /// The PostgreSQL type a plain scalar travels as, as the compiler's type
78    /// inference spells it; `None` for anything polymorphic or composite.
79    pub fn scalar_pg_type(&self) -> Option<&'static str> {
80        use PylonType::*;
81        Some(match self {
82            Str => "text",
83            Bool => "boolean",
84            Int16 => "int2",
85            Int32 => "int4",
86            Int64 => "int8",
87            Float32 => "float4",
88            Float64 => "float8",
89            Decimal | BigInt => "numeric",
90            Uuid => "uuid",
91            Json => "jsonb",
92            Bytes => "bytea",
93            Datetime => "timestamptz",
94            Duration | RelativeDuration | DateDuration => "interval",
95            LocalDatetime => "timestamp",
96            LocalDate => "date",
97            LocalTime => "time",
98            _ => return None,
99        })
100    }
101
102    /// PyQL-facing spelling of the type, as a user would write it in a query
103    /// (`str`, `array<int64>`, `range<datetime>`). Distinct from
104    /// `ddl::pg_type`, which renders the PostgreSQL side.
105    pub fn pyql_name(&self) -> String {
106        use PylonType::*;
107        match self {
108            Str => "str".into(),
109            Bool => "bool".into(),
110            Int16 => "int16".into(),
111            Int32 => "int32".into(),
112            Int64 => "int64".into(),
113            Float32 => "float32".into(),
114            Float64 => "float64".into(),
115            Decimal => "decimal".into(),
116            BigInt => "bigint".into(),
117            Uuid => "uuid".into(),
118            Json => "json".into(),
119            Bytes => "bytes".into(),
120            Datetime => "datetime".into(),
121            Duration => "duration".into(),
122            LocalDatetime => "cal::local_datetime".into(),
123            LocalDate => "cal::local_date".into(),
124            LocalTime => "cal::local_time".into(),
125            RelativeDuration => "cal::relative_duration".into(),
126            DateDuration => "cal::date_duration".into(),
127            Vector => "pgvector::vector".into(),
128            Geometry => "postgis::geometry".into(),
129            Geography => "postgis::geography".into(),
130            Box2D => "postgis::box2d".into(),
131            Box3D => "postgis::box3d".into(),
132            Any => "any".into(),
133            AnyOrderable => "anyorderable".into(),
134            AnyPoint => "anypoint".into(),
135            Array(inner) => format!("array<{}>", inner.pyql_name()),
136            Set(inner) => format!("set<{}>", inner.pyql_name()),
137            Optional(inner) => format!("optional<{}>", inner.pyql_name()),
138            Range(inner) => format!("range<{}>", inner.pyql_name()),
139            Multirange(inner) => format!("multirange<{}>", inner.pyql_name()),
140            Tuple(ts) => format!(
141                "tuple<{}>",
142                ts.iter().map(|t| t.pyql_name()).collect::<Vec<_>>().join(", ")
143            ),
144        }
145    }
146
147    /// True for a set-typed position — the marker that distinguishes an
148    /// aggregate parameter (`std::count(set<any>)`) or a set-returning
149    /// result (`std::array_unpack`) from an ordinary scalar one.
150    pub fn is_set(&self) -> bool {
151        matches!(self, PylonType::Set(_))
152    }
153}
154
155// ── PylonFunction definition ─────────────────────────────────────────────────
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum SqlLanguage {
159    Sql,
160    PlPgSql,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum FnVolatility {
165    /// Same arguments always produce the same result — safe anywhere.
166    Immutable,
167    /// Result is fixed within a single statement, but may vary between
168    /// statements (session settings, current transaction time).
169    Stable,
170    /// Result may differ on every call (`random()`, `uuidv7()`,
171    /// `clock_timestamp()`). Wanted in a pointer default, almost always a
172    /// bug inside a filter predicate.
173    Volatile,
174    /// Volatile *and* side-effecting — advancing or resetting a sequence.
175    /// Never admissible in an expression the caller expects to be a pure
176    /// predicate, since it would fire once per row.
177    Modifying,
178}
179
180/// The SQL definition for one `_pylon` schema function overload.
181///
182/// Each `FnDescriptor` with `ImplStrategy::PylonFunction(def)` installs a
183/// separate overload in PostgreSQL — PG resolves them by argument types.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct PylonFnDef {
186    /// Unqualified name in the `_pylon` schema, e.g. `"to_bool"`.
187    pub name: &'static str,
188    pub language: SqlLanguage,
189    pub volatility: FnVolatility,
190    /// When false the function is called even when arguments are NULL.
191    /// Required for optional `msg` parameters that legitimately accept NULL.
192    pub strict: bool,
193    /// Override for the PostgreSQL RETURNS clause. When `None`, derived from
194    /// the owning `FnDescriptor`'s `return_type`.
195    pub returns_override: Option<&'static str>,
196    /// SQL body — the content between `$$` delimiters.
197    pub body: &'static str,
198}
199
200// ── Implementation strategy ──────────────────────────────────────────────────
201
202/// How the transpiler should emit a stdlib function call.
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum ImplStrategy {
205    /// Delegates to a named PostgreSQL built-in. No `_pylon` function installed.
206    SqlBuiltin(&'static str),
207    /// Inline SQL template; `$1`, `$2`, … are positional placeholders.
208    SqlExpression(&'static str),
209    /// Maps to a SQL infix operator; transpiler emits `$1 op $2`.
210    SqlOperator(&'static str),
211    /// Installs a function in the `_pylon` schema via `export_stdlib()`.
212    PylonFunction(PylonFnDef),
213    /// Special transpiler rewriting — no `_pylon` function is installed.
214    /// The transpiler substitutes type-specific PG expressions at compile time
215    /// (e.g. `range` → `int8range(...)`, `multirange` → `int8multirange(...)`).
216    TranspilerIntrinsic(&'static str),
217}
218
219// ── Parameter ────────────────────────────────────────────────────────────────
220
221#[derive(Debug, Clone)]
222pub struct Param {
223    pub name: &'static str,
224    pub ty: PylonType,
225    /// True for `name: type...` variadic parameters.
226    pub variadic: bool,
227    /// `named only name: type = default` — passed only as `name := value`,
228    /// and this value when left out.
229    pub named_only: Option<NamedDefault>,
230    /// The name a call passes it by when that is not `name` -- which is also
231    /// the SQL function's parameter name, and so cannot change once
232    /// installed (`CREATE OR REPLACE` refuses a renamed parameter).
233    pub keyword: Option<&'static str>,
234}
235
236impl Param {
237    /// The name a call passes this parameter by.
238    pub fn keyword(&self) -> &'static str {
239        self.keyword.unwrap_or(self.name)
240    }
241}
242
243/// What a named-only parameter stands at when a call leaves it out.
244#[derive(Debug, Clone, Copy)]
245pub enum NamedDefault {
246    Int(i64),
247    Bool(bool),
248    Str(&'static str),
249    /// No default at all — the call has to pass it by name.
250    Required,
251    /// `<str>{}` — no value.
252    Empty,
253}
254
255// ── Function descriptor ──────────────────────────────────────────────────────
256
257/// One overload of a stdlib function.
258#[derive(Debug, Clone)]
259pub struct FnDescriptor {
260    /// PyQL namespace: `"std"`, `"math"`, or `"cal"`.
261    pub namespace: &'static str,
262    /// Unqualified function name, e.g. `"count"`, `"str_lower"`.
263    pub name: &'static str,
264    /// Ordered parameter list for this overload.
265    pub params: Vec<Param>,
266    /// Return type for this overload.
267    pub return_type: PylonType,
268    /// How the transpiler should emit this call.
269    pub impl_strategy: ImplStrategy,
270    /// True when this overload backs a `Function`-strategy type cast entry.
271    pub cast_target: bool,
272    /// Call-result stability, independent of `impl_strategy` — a
273    /// `SqlBuiltin` like `uuidv7()` is volatile even though it installs no
274    /// `PylonFnDef` of its own. Consumers gate on this: a pointer default
275    /// *wants* volatile, a filter predicate almost never does.
276    pub volatility: FnVolatility,
277}
278
279// ── Static registry ──────────────────────────────────────────────────────────
280
281static STDLIB: OnceLock<Vec<FnDescriptor>> = OnceLock::new();
282
283/// Return the full stdlib registry, initializing it on first call.
284pub fn registry() -> &'static [FnDescriptor] {
285    STDLIB.get_or_init(registry::build)
286}
287
288impl FnVolatility {
289    /// Lowercase wire name, as consumed by the Python `std` namespace gate.
290    pub fn as_str(&self) -> &'static str {
291        match self {
292            FnVolatility::Immutable => "immutable",
293            FnVolatility::Stable => "stable",
294            FnVolatility::Volatile => "volatile",
295            FnVolatility::Modifying => "modifying",
296        }
297    }
298}
299
300impl FnDescriptor {
301    /// True when any parameter is set-typed — i.e. this overload is an
302    /// aggregate and only makes sense over a multilink path or a subquery,
303    /// never over a single scalar pointer.
304    pub fn is_aggregate(&self) -> bool {
305        self.params.iter().any(|p| p.ty.is_set())
306    }
307
308    /// True when the call yields a set rather than a single value
309    /// (`std::array_unpack`) — meaningless inside a filter predicate.
310    pub fn returns_set(&self) -> bool {
311        self.return_type.is_set()
312    }
313
314    /// True for `TranspilerIntrinsic` overloads, which need compile-time type
315    /// context and so can't be validated by arity alone.
316    pub fn is_intrinsic(&self) -> bool {
317        matches!(self.impl_strategy, ImplStrategy::TranspilerIntrinsic(_))
318    }
319
320    /// True when the overload accepts a trailing variadic parameter, so any
321    /// argument count at or above `params.len() - 1` is legal.
322    ///
323    /// Trailing among the *positional* parameters: `json_set(target, path...,
324    /// value := …)` declares named-only ones after the variadic, and they are
325    /// passed by name rather than counted against it.
326    pub fn is_variadic(&self) -> bool {
327        self.params
328            .iter()
329            .rev()
330            .find(|p| p.named_only.is_none())
331            .is_some_and(|p| p.variadic)
332    }
333
334    /// Index of the variadic parameter, when one is declared.
335    pub fn variadic_index(&self) -> Option<usize> {
336        self.params.iter().position(|p| p.variadic)
337    }
338
339    /// Count of parameters a call passes by name rather than by position.
340    pub fn named_count(&self) -> usize {
341        self.params.iter().filter(|p| p.named_only.is_some()).count()
342    }
343}
344
345/// Return all overloads for the given namespace + name pair.
346pub fn lookup(namespace: &str, name: &str) -> Vec<&'static FnDescriptor> {
347    registry()
348        .iter()
349        .filter(|f| f.namespace == namespace && f.name == name)
350        .collect()
351}
352
353/// Enums the stdlib itself defines, as opposed to a schema's own. Kept here
354/// rather than in a `SchemaDescriptor` because they have no PostgreSQL enum
355/// type behind them — no migration creates one — so a member compiles to a
356/// plain `text` literal that the function consuming it switches on.
357///
358/// Member order is part of the contract, not an implementation detail:
359/// `enum_values` is `["Little", "Big"]`.
360static STDLIB_ENUMS: OnceLock<Vec<crate::schema::EnumDescriptor>> = OnceLock::new();
361
362pub fn stdlib_enums() -> &'static [crate::schema::EnumDescriptor] {
363    STDLIB_ENUMS.get_or_init(|| {
364        vec![
365            crate::schema::EnumDescriptor {
366                name: "Endian".to_string(),
367                module: "std".to_string(),
368                members: vec!["Little".to_string(), "Big".to_string()],
369            },
370            crate::schema::EnumDescriptor {
371                name: "JsonEmpty".to_string(),
372                module: "std".to_string(),
373                members: ["ReturnEmpty", "ReturnTarget", "Error", "UseNull", "DeleteKey"]
374                    .map(str::to_string)
375                    .to_vec(),
376            },
377            crate::schema::EnumDescriptor {
378                name: "Base64Alphabet".to_string(),
379                module: "enc".to_string(),
380                members: vec!["standard".to_string(), "urlsafe".to_string()],
381            },
382        ]
383    })
384}
385
386/// Resolve a stdlib enum by bare (`Endian`) or qualified (`std::Endian`) name.
387pub fn lookup_enum(name: &str) -> Option<&'static crate::schema::EnumDescriptor> {
388    stdlib_enums()
389        .iter()
390        .find(|e| e.name == name || format!("{}::{}", e.module, e.name) == name)
391}
392
393/// Iterate over every overload that backs a `Function`-strategy cast.
394pub fn cast_targets() -> impl Iterator<Item = &'static FnDescriptor> {
395    registry().iter().filter(|f| f.cast_target)
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use std::collections::HashMap;
402
403    /// Volatility is marked per *overload*, so a multi-overload name is easy
404    /// to half-annotate — `std::sequence_reset` has two, and marking only one
405    /// left the other claiming to be immutable. Callers gate on volatility,
406    /// so a single missed overload is a real hole rather than cosmetic.
407    /// Nothing in the stdlib legitimately varies volatility across overloads
408    /// of one name, so requiring agreement costs nothing and closes the gap.
409    #[test]
410    fn volatility_is_consistent_across_overloads() {
411        let mut seen: HashMap<(&str, &str), FnVolatility> = HashMap::new();
412        for d in registry() {
413            let key = (d.namespace, d.name);
414            match seen.get(&key) {
415                Some(existing) => assert_eq!(
416                    *existing, d.volatility,
417                    "{}::{} declares more than one volatility across its overloads ({:?} vs {:?}) \
418                     — every overload of a name must agree",
419                    d.namespace, d.name, existing, d.volatility,
420                ),
421                None => {
422                    seen.insert(key, d.volatility);
423                }
424            }
425        }
426    }
427
428    /// A `PylonFunction` carries its own volatility for DDL emission; the
429    /// descriptor carries one for the call gate. They describe the same
430    /// function and must not drift apart.
431    #[test]
432    fn descriptor_volatility_matches_pylon_fn_def() {
433        for d in registry() {
434            if let ImplStrategy::PylonFunction(def) = &d.impl_strategy {
435                assert_eq!(
436                    def.volatility, d.volatility,
437                    "{}::{} declares {:?} on its PylonFnDef but {:?} on its descriptor",
438                    d.namespace, d.name, def.volatility, d.volatility,
439                );
440            }
441        }
442    }
443}