razeri-plugin-contract 0.7.0

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! **One row holding an array becomes one row per element** — the vocabulary
//! an engine answers a fan-out in.
//!
//! Razeri composes the load: it knows the relation being read, the alias that
//! relation was given, the array column and the element fields the statement
//! needs. What it cannot know is how this engine turns an array into rows,
//! because no two families spell it the same way — a lateral `unnest`, a JSON
//! table function, an array join, a lateral view. So Razeri asks, once, and
//! the engine answers with the whole shape: the `FROM` item that produces the
//! rows, the expression that reads an element's position, and one expression
//! per field.
//!
//! ## Why the three parts are one answer
//!
//! They cannot be asked for separately. At least one engine needs two table
//! functions applied in series — the first produces the elements, the second
//! types their fields — so its field expressions name an alias its own `FROM`
//! item introduced. An interface that asked three questions would have no
//! place to carry that dependency, and the caller would be composing a
//! statement out of parts that do not know about each other.
//!
//! ## The ordinal counts from one, everywhere
//!
//! Engines disagree about where an array starts: about half hand back a
//! position counting from one and about half from zero, and at least one hands
//! it back as text. The element's key is derived from its position, and the
//! key is hashed into the surrogate key Razeri promises is the same on every
//! engine — so an element that is position 1 in one warehouse and position 0
//! in another would carry two different keys for one row. Normalizing in the
//! answer is what keeps that promise true, and it costs the engine that counts
//! from zero one addition.

use crate::data_type::DataType;

/// What Razeri is asking to have flattened.
///
/// Every name here is spelled the way it already appears in the statement
/// being composed — qualified where the statement qualifies it, quoted where
/// the statement quotes it. An implementation interpolates them as they are
/// rather than re-spelling them, because the answer has to name the same
/// relation the rest of the statement names.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FlattenRequest<'a> {
    /// The relation being read, qualified, as the statement's `FROM` spells it.
    pub source_relation: &'a str,
    /// The alias the recompute gave that relation.
    pub source_alias: &'a str,
    /// The array column, qualified by the source alias.
    pub array_column: &'a str,
    /// What the column was declared to hold. An array of records and an array
    /// of scalars are both flattened; on engines that fold either to a
    /// document type, this is how an implementation tells a native container
    /// from a document holding a list.
    pub array_type: &'a DataType,
    /// The alias to give one element.
    pub element_alias: &'a str,
    /// The alias to give the element's position.
    pub ordinal_alias: &'a str,
    /// The element fields the statement reads, in declared order. Empty for an
    /// array of scalars, where the element is itself the value.
    pub fields: &'a [FlattenField<'a>],
}

impl<'a> FlattenRequest<'a> {
    /// Build a request. Every part is required, because every engine that can
    /// answer at all needs all of them.
    #[must_use]
    pub fn new(
        source_relation: &'a str,
        source_alias: &'a str,
        array_column: &'a str,
        array_type: &'a DataType,
        element_alias: &'a str,
        ordinal_alias: &'a str,
        fields: &'a [FlattenField<'a>],
    ) -> Self {
        Self {
            source_relation,
            source_alias,
            array_column,
            array_type,
            element_alias,
            ordinal_alias,
            fields,
        }
    }

    /// **What one element holds**, where the column was declared as an array.
    ///
    /// An engine whose elements are untyped reads them through this: an array
    /// of plain values is answered with one expression, and the expression has
    /// to say what kind of value it is producing. `None` is a column that was
    /// not declared as an array at all, which razeri refuses before it gets
    /// here.
    #[must_use]
    pub fn element_type(&self) -> Option<&'a DataType> {
        match self.array_type {
            DataType::Array(array) => Some(array.element_type.as_ref()),
            _ => None,
        }
    }

    /// **True when the elements are records with named fields.**
    ///
    /// An array of scalars asks for no fields, because the element is the
    /// value: the answer's `fields` is then empty and the element expression
    /// reads the whole of it.
    #[must_use]
    pub fn elements_have_fields(&self) -> bool {
        !self.fields.is_empty()
    }
}

/// One field of an element, by the name the declared type gives it.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FlattenField<'a> {
    /// The field's name inside the element, unquoted, exactly as the declared
    /// type spells it. An implementation quotes it the way its engine wants,
    /// which for most is a JSON path rather than an identifier.
    pub name: &'a str,
    /// The type the target column declares for it, which is the type the read
    /// is cast to on engines whose elements are untyped.
    pub data_type: &'a DataType,
}

impl<'a> FlattenField<'a> {
    /// Name one field of an element and the type it is read as.
    #[must_use]
    pub fn new(name: &'a str, data_type: &'a DataType) -> Self {
        Self { name, data_type }
    }
}

/// How this engine spells the fan-out.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Flatten {
    /// The `FROM` item producing one row per element, written to follow the
    /// source relation and its alias. An engine that needs two table functions
    /// applied in series returns both here, as one piece of text.
    ///
    /// Razeri writes `FROM <source_relation> AS <source_alias>` and then this,
    /// so an implementation begins with whatever its engine puts between the
    /// two — a comma for a lateral join in the `FROM` list, nothing at all for
    /// an engine whose fan-out is a clause of its own.
    pub from_item: String,
    /// Reads the element's position. **One for the first element**, whatever
    /// the engine counts from.
    pub ordinal: String,
    /// One expression per requested field, in the order asked. An array of
    /// scalars is answered with exactly one expression, reading the element
    /// whole.
    pub fields: Vec<String>,
}

impl Flatten {
    /// State a fan-out: the `FROM` item, the one-based ordinal, and one
    /// expression per field asked for.
    #[must_use]
    pub fn new(
        from_item: impl Into<String>,
        ordinal: impl Into<String>,
        fields: Vec<String>,
    ) -> Self {
        Self { from_item: from_item.into(), ordinal: ordinal.into(), fields }
    }
}

#[cfg(test)]
mod tests {
    use super::{Flatten, FlattenField, FlattenRequest};
    use crate::agnostic::AnsiDialect;
    use crate::data_type::DataType;
    use crate::renderer::DmlRenderer;

    /// The ANSI renderer stands in for "an engine that has answered nothing":
    /// every method of the DML trait carries a default, so an empty
    /// implementation is a whole dialect's DML, and the flatten it states is
    /// the one a plugin author gets for free.
    impl DmlRenderer for AnsiDialect {}

    /// A dialect that says nothing says `None`, and Razeri has a refusal to
    /// write rather than a statement to guess at.
    #[test]
    fn an_engine_that_states_nothing_answers_none() {
        let array = DataType::json();
        let request = FlattenRequest::new(
            "\"raw\".\"orders\"",
            "\"src\"",
            "\"src\".\"items\"",
            &array,
            "\"elem\"",
            "\"_ordinal\"",
            &[],
        );
        assert!(AnsiDialect.render_flatten(&request).is_none());
    }

    /// An array of scalars asks for no fields: the element is the value, and
    /// the answer carries exactly one expression for it.
    #[test]
    fn an_array_of_scalars_asks_for_no_fields() {
        let array = DataType::json();
        let request = FlattenRequest::new(
            "\"raw\".\"pings\"",
            "\"src\"",
            "\"src\".\"branches\"",
            &array,
            "\"elem\"",
            "\"_ordinal\"",
            &[],
        );
        assert!(!request.elements_have_fields());
        assert!(request.element_type().is_none(), "a document column declares no element type");

        let answered = Flatten::new(", unnest(\"src\".\"branches\")", "1", vec!["\"elem\"".into()]);
        assert_eq!(answered.fields.len(), 1);
    }

    /// An array of records asks for one expression per field, in the order the
    /// table declared them.
    #[test]
    fn an_array_of_records_asks_for_one_expression_per_field() {
        let product = DataType::varchar(10);
        let quantity = DataType::int();
        let fields =
            [FlattenField::new("product_id", &product), FlattenField::new("quantity", &quantity)];
        let array = DataType::json();
        let request = FlattenRequest::new(
            "\"raw\".\"orders\"",
            "\"src\"",
            "\"src\".\"items\"",
            &array,
            "\"elem\"",
            "\"_ordinal\"",
            &fields,
        );
        assert!(request.elements_have_fields());
        assert_eq!(
            request.fields.iter().map(|field| field.name).collect::<Vec<_>>(),
            ["product_id", "quantity"]
        );
    }
}