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}