selene-db-gql 1.3.0

ISO/IEC 39075:2024 GQL parser, planner, optimizer, and executor for selene-db.
Documentation
//! GQL type AST nodes.

use std::hash::{Hash, Hasher};

use selene_core::{
    DbString, DecimalType, MAX_BYTE_STRING_TYPE_LENGTH, MAX_CHARACTER_STRING_TYPE_LENGTH,
};

/// Parsed GQL type.
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[non_exhaustive]
pub enum GqlType {
    /// `ANY`.
    Any,
    /// `PROPERTY VALUE`.
    AnyProperty,
    /// Closed dynamic union value type.
    ClosedDynamicUnion(Vec<GqlType>),
    /// `STRING`.
    String,
    /// Bounded character-string type.
    CharacterString(CharacterStringType),
    /// `BOOLEAN`.
    Boolean,
    /// `INTEGER`.
    Integer,
    /// `FLOAT`.
    ///
    /// Width-generic floating-point type. Consumers deriving storage or
    /// conversion maps from parsed GQL types must accept both `f32` and `f64`
    /// values unless a narrower `FLOAT32` or `FLOAT64` was requested.
    Float,
    /// `INT8`.
    Int8,
    /// `INT16`.
    Int16,
    /// `INT32`.
    Int32,
    /// `INT64`.
    Int64,
    /// `INT128`.
    Int128,
    /// `UINT8`.
    Uint8,
    /// `UINT16`.
    Uint16,
    /// `UINT32`.
    Uint32,
    /// `UINT64`.
    Uint64,
    /// `UINT128`.
    Uint128,
    /// `USMALLINT`.
    USmallInt,
    /// `UINT`.
    Uint,
    /// `UBIGINT`.
    UBigInt,
    /// `SMALLINT`.
    SmallInt,
    /// `BIGINT`.
    BigInt,
    /// `DECIMAL`.
    Decimal,
    /// `DECIMAL(p)` or `DECIMAL(p, s)`.
    DecimalExact(DecimalType),
    /// `FLOAT32`.
    Float32,
    /// `FLOAT64`.
    Float64,
    /// `REAL`.
    ///
    /// ISO floating-point type-name synonym with `FLOAT32` semantics.
    Real,
    /// `DOUBLE` or `DOUBLE PRECISION`.
    ///
    /// ISO floating-point type-name synonym with `FLOAT64` semantics.
    Double,
    /// Byte-string type.
    Bytes,
    /// Bounded byte-string type.
    ByteString(ByteStringType),
    /// `UUID`.
    Uuid,
    /// Native JSON value.
    Json,
    /// `ZONED DATETIME`.
    ZonedDateTime,
    /// `LOCAL DATETIME`.
    LocalDateTime,
    /// `DATE`.
    Date,
    /// `ZONED TIME`.
    ZonedTime,
    /// `LOCAL TIME`.
    LocalTime,
    /// Internal broad duration value family.
    ///
    /// Parsed duration type names use the qualified variants below.
    Duration,
    /// `DURATION (YEAR TO MONTH)`.
    DurationYearToMonth,
    /// `DURATION (DAY TO SECOND)`.
    DurationDayToSecond,
    /// Native dense-vector value.
    ///
    /// This internal type is used by procedure metadata and typed parameter
    /// validation. It is not parsed as a GQL type name; vector syntax remains
    /// outside the ISO grammar surface.
    Vector,
    /// `RECORD`.
    Record(RecordType),
    /// `LIST<T>`.
    List(Box<GqlType>),
    /// `LIST<T>[n]`.
    BoundedList {
        /// Element value type.
        element_type: Box<GqlType>,
        /// Maximum list cardinality.
        max_len: u64,
    },
    /// Explicitly non-null value type (`<value type> NOT NULL`).
    NotNull(Box<GqlType>),
    /// `PATH`.
    Path,
    /// Graph reference.
    GraphRef,
    /// Node reference.
    NodeRef,
    /// Edge reference.
    EdgeRef,
    /// Binding-table reference.
    TableRef(BindingTableType),
    /// `NULL`.
    Null,
    /// `NOTHING`.
    Nothing,
}

/// Binding table type carried by a binding-table reference value type.
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum BindingTableType {
    /// Internal unconstrained table reference used where a procedure surface
    /// cannot statically describe the returned table's field set.
    Any,
    /// ISO `[BINDING] TABLE <field types specification>`.
    Closed(Vec<(DbString, GqlType)>),
}

/// Parsed bounded character-string type metadata.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct CharacterStringType {
    /// Minimum character length accepted by the type.
    pub min_len: u64,
    /// Maximum character length accepted by the type.
    pub max_len: u64,
    /// Parsed syntactic form used for feature stamping.
    pub form: CharacterStringTypeForm,
}

/// Parsed bounded character-string syntactic form.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum CharacterStringTypeForm {
    /// `STRING(max)`.
    StringMax,
    /// `STRING(min,max)`.
    StringMinMax,
    /// `CHAR(fixed)` or bare `CHAR`.
    CharFixed,
    /// `VARCHAR(max)`.
    VarcharMax,
}

impl CharacterStringType {
    /// Construct a character-string type when the length bounds are valid
    /// and within [`MAX_CHARACTER_STRING_TYPE_LENGTH`]. Fixed-length coercion
    /// pads up to `min_len`, so the declared-length cap is the allocation
    /// bound for every downstream CAST/assignment/default funnel.
    #[must_use]
    pub const fn new(min_len: u64, max_len: u64, form: CharacterStringTypeForm) -> Option<Self> {
        if max_len == 0 || min_len > max_len || max_len > MAX_CHARACTER_STRING_TYPE_LENGTH {
            return None;
        }
        Some(Self {
            min_len,
            max_len,
            form,
        })
    }

    /// Return true if this type is fixed-length.
    #[must_use]
    pub const fn is_fixed_length(&self) -> bool {
        self.min_len == self.max_len
    }
}

impl PartialEq for CharacterStringType {
    fn eq(&self, other: &Self) -> bool {
        self.min_len == other.min_len && self.max_len == other.max_len
    }
}

impl Eq for CharacterStringType {}

impl Hash for CharacterStringType {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.min_len.hash(state);
        self.max_len.hash(state);
    }
}

/// Parsed bounded byte-string type metadata.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct ByteStringType {
    /// Minimum byte length accepted by the type.
    pub min_len: u64,
    /// Maximum byte length accepted by the type.
    pub max_len: u64,
    /// Parsed syntactic form used for feature stamping.
    pub form: ByteStringTypeForm,
}

/// Parsed bounded byte-string syntactic form.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum ByteStringTypeForm {
    /// `BYTES(max)`.
    BytesMax,
    /// `BYTES(min,max)`.
    BytesMinMax,
    /// `BINARY(fixed)`.
    BinaryFixed,
    /// `VARBINARY(max)`.
    VarbinaryMax,
}

impl ByteStringType {
    /// Construct a byte-string type when the length bounds are valid and
    /// within [`MAX_BYTE_STRING_TYPE_LENGTH`]. Fixed-length coercion pads up
    /// to `min_len`, so the declared-length cap is the allocation bound for
    /// every downstream CAST/assignment/default funnel.
    #[must_use]
    pub const fn new(min_len: u64, max_len: u64, form: ByteStringTypeForm) -> Option<Self> {
        if max_len == 0 || min_len > max_len || max_len > MAX_BYTE_STRING_TYPE_LENGTH {
            return None;
        }
        Some(Self {
            min_len,
            max_len,
            form,
        })
    }

    /// Return true if this type is fixed-length.
    #[must_use]
    pub const fn is_fixed_length(&self) -> bool {
        self.min_len == self.max_len
    }
}

impl PartialEq for ByteStringType {
    fn eq(&self, other: &Self) -> bool {
        self.min_len == other.min_len && self.max_len == other.max_len
    }
}

impl Eq for ByteStringType {}

impl Hash for ByteStringType {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.min_len.hash(state);
        self.max_len.hash(state);
    }
}

/// Parsed record type.
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[non_exhaustive]
pub enum RecordType {
    /// Open record.
    Open,
    /// Closed record with named fields.
    Closed(Vec<(DbString, GqlType)>),
}

impl GqlType {
    /// Return the underlying value type after removing explicit `NOT NULL` wrappers.
    #[must_use]
    pub fn strip_not_null(&self) -> &Self {
        let mut ty = self;
        while let Self::NotNull(inner) = ty {
            ty = inner;
        }
        ty
    }

    /// Return true when this type is any duration family.
    #[must_use]
    pub fn is_duration(&self) -> bool {
        matches!(
            self.strip_not_null(),
            Self::Duration | Self::DurationYearToMonth | Self::DurationDayToSecond
        )
    }
}