Sign in

    partiql-value

    The PartiQL data model for MoonBit, with PartiQL-encoded Ion, JSON and YAML input codecs.

    partiql
    sql
    query
    ion
    data-model
    Download zip
    Author
    Version
    0.3.0
    License
    Apache-2.0
    Last updated
    4 days ago
    Downloads
    35

    #moonrockz/partiql-value

    The PartiQL data model for MoonBit, with structural identity, SQL equality, grouping equivalence, ordering, and codecs for PartiQL-encoded Ion, JSON and YAML input.

    #Installation and imports

    moon add moonrockz/partiql-value

    Add the packages you use to moon.pkg:

    import {
    "moonrockz/partiql-value" @value,
    "moonrockz/partiql-value/ion" @partiql_ion,
    "moonrockz/partiql-value/json" @partiql_json,
    "moonrockz/partiql-value/yaml" @partiql_yaml,
    "moonrockz/ion/ion" @ion_core,
    }

    The checked examples use moonrockz/expect for assertions. Add it with moon add moonrockz/expect and put this import in moon.pkg when copying the test blocks:

    import { "moonrockz/expect" } for "test"

    PackageContent
    moonrockz/partiql-valueValue, collections, maps, graphs, datetime and interval types, and value relations
    moonrockz/partiql-value/ionSingle-value codecs, text/binary datagrams and Ion Schema validation
    moonrockz/partiql-value/jsonStrict JSON documents with exact numbers and duplicate keys
    moonrockz/partiql-value/yamlYAML 1.2 Core input, exact numbers and bounded aliases
    moonrockz/partiql-value/arbitraryQuickcheck generators and shrinkable wrappers

    #Constructing values

    Value(x) converts Bool, Int, Int64, BigInt, Double, String, Ion decimals, validated datetime/interval types, Bag, Tuple, MapValue, GraphValue and arrays of convertible elements. An existing Value passes through. A BigInt becomes Int when it fits Int64. An Ion value is boxed without lowering it.

    test "construct values" {
    let list = @value.Value([1, 2, 3])
    @expect.expect(list).to_equal(@value.Value::from_list([@value.Value::from_int(1), @value.Value::from_int(2), @value.Value::from_int(3)]))
    }

    Use Value::from_* when the payload type is known. Every payload-bearing variant has a typed conversion:

    PayloadConversion
    Bool, Int64, BigIntfrom_bool, from_int, from_bigint
    Double, Ion Decimal, Stringfrom_float, from_decimal, from_string
    Bytesfrom_blob or from_clob, chosen explicitly
    Date, Time, Timestampfrom_date, from_time, from_timestamp
    IntervalYM, IntervalDTfrom_interval_ym, from_interval_dt
    Array[Value], Bag, Tuplefrom_list, from_bag, from_tuple
    MapValue, GraphValue, Ion IonValuefrom_map, from_graph, from_ion

    from_bigint normalizes integers to Int when they fit Int64. Other typed conversions retain the payload's kind and storage. Domain constructors validate inputs; value conversions wrap the validated result. from_list shares its backing array, while generic Value(array) maps into a new list.

    The concrete parameter types resolve nested domain constructor names:

    test "typed bag and date conversions" {
    let bag = @value.Value::from_bag(@value.Bag([@value.Value(1), @value.Value(2)]))
    let date = @value.Value::from_date(@value.Date(year=2024, month=2, day=29))
    @expect.expect_all(soft => {
    soft.expect(bag is @value.Value::Bag(b) && b.length() == 2).to_be_true()
    soft.expect(date is @value.Value::Date(d) && d.day == 29).to_be_true()
    })
    }

    Tuple(fields) builds the domain tuple. Value::from_tuple(tuple) wraps it as a PartiQL value and is equivalent to Value(tuple):

    test "build and wrap a tuple" {
    let value = @value.Value::from_tuple(@value.Tuple([
    ("a", @value.Value(1)), ("a", @value.Value(2)),
    ]))
    guard value is @value.Value::Tuple(tuple) else {
    fail("expected a tuple value")
    }
    @expect.expect(tuple.get_all("a")).to_equal([@value.Value(1), @value.Value(2)])
    }

    Use enum variants in patterns, as above. They also remain available for construction for compatibility. Bag, Tuple, Date, Time, Timestamp, IntervalYM and IntervalDT share names with variants, so a standalone let tuple = @value.Tuple(fields) still needs an expected result type or qualified Tuple::Tuple(...). Prefer typed wrapping functions when building a Value, for example Value::from_tuple(@value.Tuple(fields)).

    A tuple preserves duplicate names and drops MISSING fields. A bag keeps MISSING and duplicates. Bags retain the supplied backing array; copy it before construction if you will mutate it later. Tuple, map and graph constructors create their own arrays. Collection accessors return read-only views in stored order.

    Use explicit Value::Null and Value::Missing. Bytes have two meanings, so choose Value::from_blob(bytes) or Value::from_clob(bytes) explicitly. Arrays become lists; use a Bag for an unordered collection. Conversions do not perform SQL CAST coercions.

    Implement ToValue to support a domain type with Value(x):

    struct CustomerId(Int)

    impl @value.ToValue for CustomerId with fn to_value(self) {
    @value.Value(self.0)
    }

    test "domain conversion" {
    @expect.expect(@value.Value(CustomerId(7))).to_equal(@value.Value::from_int(7))
    }

    The former Type::new(...) entry points remain deprecated aliases. Replace them with Type(...), or Type::Type(...) where qualification is needed. Value::integer remains available as the legacy spelling of Value::from_bigint.

    #Datetimes and intervals

    Dates use the proleptic Gregorian calendar with years -999999999..999999999. Times retain fractional precision and an optional offset in minutes east of UTC; None means WITHOUT TIME ZONE. Invalid fields raise DateTimeError. Intervals normalize their parts and raise IntervalError::Overflow when the representation cannot hold the result.

    test "datetime and interval constructors" {
    let date : @value.Date = @value.Date(year=2024, month=2, day=29)
    let time : @value.Time = @value.Time(
    hour=12, minute=30, second=0, nanos=125000000, precision=3, offset=Some(60),
    )
    let timestamp : @value.Timestamp = @value.Timestamp(date, time)
    let interval : @value.IntervalDT = @value.IntervalDT(0, -250000000)
    @expect.expect((timestamp.time.precision, interval.is_negative(), interval.nanos_part())).to_equal((3, true, 250000000L))
    }

    #Maps and graphs

    MapValue checks keys against the declared key family and rejects NULL, MISSING, nonscalar and duplicate keys. Duplicate keys use grouping equivalence. MISSING values are dropped after all keys are checked. The declared value type is metadata; construction does not coerce values to it.

    Graphs have separate node and edge id namespaces. Construction rejects unknown endpoints, duplicate ids within a namespace and MISSING payloads. Labels are deduplicated; absent payloads use None.

    test "map and graph constructors" {
    let map = @value.MapValue(
    key_type=@value.MapType::String, value_type=@value.MapType::Dynamic,
    [(@value.Value("a"), @value.Value(1))],
    )
    let node = @value.GraphNode(id="n", labels=["person", "person"])
    let edge = @value.GraphEdge(id="e", ends=@value.Ends::Directed(from="n", to="n"))
    let graph = @value.GraphValue([node], [edge])
    @expect.expect((map.length(), graph.nodes()[0].labels().length(), graph.edges().length())).to_equal((1, 1, 1))
    }

    #Choosing a relation

    RelationMeaning
    == via EqStructural identity: same variant and contents; bags and tuples as multisets; decimal scale counts
    sql_equalsSQL equality over values: MISSING takes precedence over NULL; numbers compare across types
    eqgGrouping equivalence: NULL equals NULL, MISSING equals MISSING, NaN equals NaN
    compareORDER BY total order, choosing NullsFirst or NullsLast

    Boxed Ion values compare through their lowered representation in semantic relations. Structural identity retains the Ion representation. The evaluator's = operator adds its own absence and datetime rules; use evaluate_text for query semantics.

    test "boxed Ion equality" {
    let symbol = @value.Value(@ion_core.IonValue::symbol("hello"))
    @expect.expect(@value.sql_equals(symbol, @value.Value("hello"))).to_equal(@value.Value::from_bool(true))
    }

    Value::lower lowers the outer Ion representation; lower_deep also lowers nested Ion. An unresolved symbol raises LowerError. fold walks deeply nested values without relying on the call stack.

    #Ion codecs and datagrams

    @partiql_ion.from_ion and to_ion read and write PartiQL-encoded Ion, including $bag, $missing and $ion. from_ion_data boxes raw Ion; to_ion_data preserves boxed Ion at every depth. Choose a matching pair: a raw $bag annotation must stay boxed when reading Ion-preserving output.

    Value::from_ion boxes raw Ion without decoding PartiQL annotations. It is equivalent to Value(ion_value) or the codec's from_ion_data, and differs from the codec's from_ion decoding operation.

    Datagram readers return ordered arrays without implicit bag conversion. read_bytes detects binary Ion by its version marker; otherwise it requires valid UTF-8 text. write_bytes writes a binary datagram with a local symbol table. write_text writes one value per line, with a final newline for nonempty input and optional pretty=true layout.

    test "binary datagram round trip" {
    let values = @partiql_ion.read_text("1 $missing::null $bag::[2, 3] $ion::foo")
    let bytes = @partiql_ion.write_bytes(values)
    @expect.expect(@partiql_ion.read_bytes(bytes)).to_equal(values)
    }

    Readers accept optional catalog~ for shared symbol tables and validation~ for Ion Schema validation. Load a schema from text or binary bytes with load_schema_bytes. Validation::elements validates each raw element; Validation::document validates a complete datagram. Validation runs before PartiQL decoding and collects violations with paths. Schemas describe physical Ion types and annotations, including $bag and $missing.

    test "validate raw Ion" {
    let schema = @partiql_ion.load_schema_bytes(
    b"$ion_schema_2_0 schema_header::{} type::{name: row, type: struct} schema_footer::{}",
    )
    let validation = @partiql_ion.Validation::elements(schema, "row")
    let values = @partiql_ion.read_text("{a: 1} {a: 2}", validation~)
    @expect.expect(values.length()).to_equal(2)
    }

    CodecError distinguishes malformed and unsupported encodings. IonDataError distinguishes UTF-8, text/binary Ion, codec, schema and output failures. Violations carries every validation path and message.

    #Property tests

    Import moonrockz/partiql-value/arbitrary and moonbitlang/core/quickcheck for tests. ArbValue, ArbIon and ArbTimestamp implement generation and shrinking. gen_value(size, state) accepts an explicit recursion budget; nonpositive sizes produce leaves. Generators include absent values, numeric boundaries, boxed Ion, maps and graphs.

    See the property testing guide for a runnable codec property and generator controls.

    See the repository's architecture, Ion semantics and datagram/session design. The checked examples above run through mise run test:docs.

    #JSON documents

    Import moonrockz/partiql-value/json as @partiql_json. read_text and read_bytes read exactly one RFC 8259 JSON document. Bytes must be UTF-8 without a BOM. Empty input, concatenated documents, JSON Lines, comments, trailing commas and Ion syntax are errors. Strings must contain Unicode scalar values, so unpaired surrogate escapes are rejected. Nesting is limited to 256 containers in both directions.

    JSONPartiQL value
    null, booleans, stringsNull, Bool, String
    Integer without fraction or exponentInt within Int64, otherwise BigInt; -0 normalizes to 0
    Fraction or exponent, such as 1.2300 or 1e2Exact Decimal, preserving coefficient, scale and signed zero; 1e2 is coefficient 1, exponent 2
    ArrayList, retaining order
    ObjectTuple, retaining field order and duplicate names

    Decimal exponents, after adjustment for fractional digits, must fit Int32. An explicit exponent must itself fit Int32. Large coefficients do not pass through binary64, so neither integers nor decimals lose precision.

    write_text(value, pretty=false) emits one document without a trailing newline. write_bytes emits its UTF-8 bytes. pretty=true uses two-space indentation. The writer emits duplicate fields, which other JSON readers may collapse.

    PartiQL value on writeJSON representation or error
    Null, Bool, String, Int, BigInt, List, TupleCorresponding JSON value; nested values follow the same rules
    DecimalExact coefficient and exponent, such as 12300e-4; scale and negative zero survive read-back
    Finite FloatShortest binary64 round-trip number spelling; negative zero is -0.0
    NaN or infinite FloatJsonError::Unsupported
    Missing, Bag, Map, GraphJsonError::Unsupported
    Date, Time, Timestamp, IntervalYM, IntervalDTJsonError::Unsupported
    Blob, Clob, boxed IonJsonError::Unsupported

    JSON has no numeric type tags. Reading a written finite Float produces an Int or Decimal, not a Float. Its spelling round-trips through a binary64 JSON consumer, but may denote a different exact decimal value from the binary fraction. Use Ion when the numeric type or exact binary representation must survive. Every value parsed by this JSON codec round-trips with structural identity, including decimal scale and duplicate fields.

    test "JSON document round trip" {
    let value = @partiql_json.read_text("{\"n\":9223372036854775808,\"price\":1.2300}")
    @expect.expect(@partiql_json.read_bytes(@partiql_json.write_bytes(value))).to_equal(value)
    }

    JsonError distinguishes invalid UTF-8, syntax errors with a zero-based UTF-16 offset, unsupported output values, and invalid binding shapes. error.message() gives a human-readable description. No unsupported value is silently omitted or replaced with null. The Tuple constructor itself removes Missing fields before the codec sees them.

    #YAML input

    yaml.read_text(StringView) and read_bytes(BytesView) return Array[Value], one value per document, in source order. Readers succeed with the whole stream or raise YamlError; they never return partial documents. Empty or comment-only input gives []; an explicit empty document gives [Null].

    test "read exact YAML documents" {
    let values = @partiql_yaml.read_text("--- {price: 1.2300}\n--- {price: -0.00}")
    @expect.expect(values).to_equal(@partiql_ion.read_text("{price: 1.2300} {price: -0.00}"))
    }

    YAML inputPartiQL value
    Core null and booleansNull and Bool
    Decimal, 0o octal and 0x hexadecimal integersInt64-backed Int, or BigInt when needed
    Finite float spellings, including 1e2Exact Decimal with coefficient, exponent and negative zero preserved
    .inf, -.inf, .nan and Core case variantsFloat special values
    Plain strings, quoted strings, literal/folded blocksString
    SequencesList
    MappingsTuple, preserving field order

    Resolution is YAML 1.2 Core: yes, no, on, off, dates, 0b10, 1_000 and 1:20 remain strings. 012 means twelve. Quoted/block text stays String unless explicitly tagged. Versions other than 1.2 are rejected. Mapping keys must resolve to String; quote a numeric-looking key or use !!str. Duplicate decoded names are rejected by exact Unicode equality, including escaped and aliased keys. No Unicode normalization is applied.

    The seven core tags !!str, !!null, !!bool, !!int, !!float, !!seq and !!map are supported, including full URIs and %TAG handles. Explicit scalar tags require Core lexical forms and collection tags require the matching node kind. !!float '1' produces Decimal; !!float '-0' retains negative zero. Bare ! forces the ordinary string/sequence/mapping kind. Unknown tags and non-core timestamp, binary, set, omap, pairs and merge tags are unsupported. Plain << is an ordinary field name, with no merge expansion.

    Anchors and acyclic aliases are supported. Each occurrence becomes an independent value tree; changing a list cannot change another occurrence. Forward/undefined aliases and cycles fail. Reused names refer to the latest preceding anchor. Anchors and tag directives reset between documents. Comments, styles and anchor names do not survive decoding. YAML output is not provided.

    Bytes must be UTF-8; YAML BOMs are accepted. UTF-16 and UTF-32 are rejected. YamlError distinguishes Encoding, Syntax, Unsupported, DuplicateKey, Limit and Bindings. Positioned errors carry a 1-based document, line and Unicode-scalar column relative to the supplied view, never a guessed byte offset.

    Fixed limits apply per input stream: 256 nested containers, 1,000,000 expanded nodes and 64 MiB of decoded scalar UTF-8 payload. Each scalar, sequence and mapping is a node; mapping keys count as scalar nodes. Payload counts decoded scalar spelling before type resolution, including keys, numeric spelling and every alias copy. Empty scalars use zero bytes. Containers contribute no scalar bytes. Anchor definitions count as their ordinary values, even if never aliased. Alias expansion checks aggregate node/payload/depth costs before copying. Decimal exponents must fit Int32 after the fractional scale is applied.

    The parser also bounds scanner work to 6,000,016 token fetches, 256 flow levels, 512 indentation-stack entries and 64 MiB per metadata buffer. Scalar and folding buffers are capped by the remaining scanned payload budget before growth. These parser limits can reject unusually large presentation metadata even when its eventual Value would be small. No materialized YAML/JSON intermediate or binary64 numeric conversion is used. Coverage is a pinned, representative YAML fixture subset, not a full-compliance claim.

    ToValue

    pub(open) trait ToValue {
    fn to_value(Self) -> Value
    }

    Conversion to a PartiQL value. Implement this trait for domain types that have one unambiguous representation, then call Value(x). Conversions preserve types; they do not perform SQL CAST coercions.
    impl ToValue for Bool
    impl ToValue for Int
    impl ToValue for Int64
    impl ToValue for Double
    impl ToValue for String
    impl ToValue for Array[T]
    impl ToValue for Ion

    DateTimeError

    pub(all) suberror DateTimeError {
    InvalidDate(String)
    InvalidTime(String)
    } derive(Eq,
    Debug
    )

    A date, time or timestamp field that is out of range.

    GraphError

    pub(all) suberror GraphError {
    DuplicateNodeId(String)
    DuplicateEdgeId(String)
    UnknownNode(String)
    MissingPayload(String)
    } derive(Eq,
    Debug
    )

    Why a graph could not be built.

    IntervalError

    pub(all) suberror IntervalError {
    Overflow(String)
    } derive(Eq,
    Debug
    )

    An interval that does not fit the representation.

    LowerError

    pub(all) suberror LowerError {
    SymbolWithoutText
    } derive(Eq,
    Debug
    )

    An Ion value that has no PartiQL equivalent.

    MapError

    pub(all) suberror MapError {
    InvalidKey(String)
    DuplicateKey(String)
    DynamicKeyType
    } derive(Eq,
    Debug
    )

    Why a MAP could not be built.

    Bag

    pub struct Bag {
    // private fields
    }

    A PartiQL bag: an unordered collection that allows duplicates.
    impl Eq for Bag
    impl Debug for Bag
    impl ToValue for Bag

    Bag::Bag

    #alias(new, deprecated="Use Bag(...) instead")
    fn Bag::Bag(items : Array[Value]) -> Bag

    Build a bag, retaining duplicates and the input order for output. The backing array is shared with items; copy it first if it will be mutated.

    Bag::items

    fn Bag::items(self : Bag) -> ArrayView[Value]

    A read-only view of the elements in their stored order, including duplicates.

    Bag::length

    fn Bag::length(self : Bag) -> Int

    The number of elements, counting duplicates.

    Date

    pub struct Date {
    year : Int
    month : Int
    day : Int
    } derive(
    Debug
    )

    A calendar date (proleptic Gregorian).
    impl Eq for Date
    impl ToValue for Date

    Date::Date

    #alias(new, deprecated="Use Date(...) instead")
    fn Date::Date(year~ : Int, month~ : Int, day~ : Int) -> Date raise DateTimeError

    Build a Gregorian date with a year in -999999999..999999999. Raise DateTimeError::InvalidDate for an invalid year, month or day, including February 29 in a non-leap year.

    test {
    let leap_day : @value.Date = @value.Date(year=2024, month=2, day=29)
    @expect.expect(leap_day.day).to_equal(29)
    }

    Ends

    pub(all) enum Ends {
    Directed(from~ : String, to~ : String)
    Undirected(String, String)
    } derive(
    Debug
    )

    The ends of a graph edge (RFC 0025): an ordered pair for a directed edge, an unordered pair for an undirected edge. Ends name node ids.
    impl Eq for Ends

    GraphEdge

    pub struct GraphEdge {
    id : String
    ends : Ends
    payload : Value?
    // private fields
    }

    A graph edge: id, labels, ends and an optional payload.

    GraphEdge::GraphEdge

    #alias(new, deprecated="Use GraphEdge(...) instead")
    fn GraphEdge::GraphEdge(id~ : String, ends~ : Ends, labels? : Array[String], payload? : Value?) -> GraphEdge

    Build an edge and remove duplicate labels in first-seen order. Endpoint existence, id uniqueness and MISSING payloads are checked by GraphValue.

    GraphEdge::labels

    fn GraphEdge::labels(self : GraphEdge) -> ArrayView[String]

    The labels, without duplicates, in first-seen order.

    GraphNode

    pub struct GraphNode {
    id : String
    payload : Value?
    // private fields
    }

    A graph node: its id from the source data, its labels (a set, kept in first-seen order) and an optional payload. An absent payload is not NULL: it acts as MISSING in queries.

    GraphNode::GraphNode

    #alias(new, deprecated="Use GraphNode(...) instead")
    fn GraphNode::GraphNode(id~ : String, labels? : Array[String], payload? : Value?) -> GraphNode

    Build a node and remove duplicate labels in first-seen order. Id uniqueness and MISSING payloads are checked when building GraphValue.

    GraphNode::labels

    fn GraphNode::labels(self : GraphNode) -> ArrayView[String]

    The labels, without duplicates, in first-seen order.

    GraphValue

    pub struct GraphValue {
    // private fields
    }

    A PartiQL graph (RFC 0025). Node ids are unique among nodes, edge ids among edges (two namespaces), and every edge end names a node.
    impl Eq for GraphValue

    GraphValue::GraphValue

    #alias(new, deprecated="Use GraphValue(...) instead")
    fn GraphValue::GraphValue(nodes : Array[GraphNode], edges : Array[GraphEdge]) -> GraphValue raise GraphError

    Build a graph from copies of the node and edge arrays. Raise GraphError for duplicate ids within either namespace, unknown endpoints, or MISSING payloads. Node and edge ids may share a name.

    GraphValue::edges

    A read-only view of edges in their input order.

    GraphValue::nodes

    A read-only view of nodes in their input order.

    IntervalDT

    pub struct IntervalDT {
    seconds : Int64
    nanos : Int
    } derive(
    Debug
    )

    A day-time interval: seconds + nanos / 10^9, with nanos in 0..999,999,999 (floor form).
    impl Eq for IntervalDT

    IntervalDT::IntervalDT

    #alias(new, deprecated="Use IntervalDT(...) instead")
    fn IntervalDT::IntervalDT(seconds : Int64, nanos : Int64) -> IntervalDT raise IntervalError

    Normalize seconds + nanos / 10^9 to floor form, carrying excess or negative nanoseconds into seconds. Raise IntervalError::Overflow if the normalized seconds or their magnitude cannot fit Int64.

    IntervalDT::days

    fn IntervalDT::days(self : IntervalDT) -> Int64

    Whole days in the absolute magnitude of the interval.

    IntervalDT::from_parts

    fn IntervalDT::from_parts(negative~ : Bool, days~ : Int64, hours~ : Int64, minutes~ : Int64, seconds~ : Int64, nanos~ : Int64) -> IntervalDT raise IntervalError

    Combine day, hour, minute, second and nanosecond parts, then negate when negative is true. Normalize overflowing parts using exact arithmetic. Raise IntervalError::Overflow if the normalized seconds or their magnitude cannot fit Int64.

    IntervalDT::hours

    fn IntervalDT::hours(self : IntervalDT) -> Int64

    Remaining hours in the absolute magnitude, in 0..23.

    IntervalDT::is_negative

    fn IntervalDT::is_negative(self : IntervalDT) -> Bool

    Whether the interval is below zero, including negative fractional seconds.

    IntervalDT::minutes

    fn IntervalDT::minutes(self : IntervalDT) -> Int64

    Remaining minutes in the absolute magnitude, in 0..59.

    IntervalDT::nanos_part

    fn IntervalDT::nanos_part(self : IntervalDT) -> Int64

    Fractional nanoseconds in the absolute magnitude, in 0..999999999.

    IntervalDT::seconds_part

    fn IntervalDT::seconds_part(self : IntervalDT) -> Int64

    Remaining whole seconds in the absolute magnitude, in 0..59.

    IntervalYM

    pub struct IntervalYM {
    months : Int64
    } derive(
    Debug
    )

    A year-month interval, as a signed number of months.
    impl Eq for IntervalYM

    IntervalYM::IntervalYM

    #alias(new, deprecated="Use IntervalYM(...) instead")
    fn IntervalYM::IntervalYM(months : Int64) -> IntervalYM raise IntervalError

    Build a signed month count. Raise IntervalError::Overflow for Int64's minimum value, whose positive magnitude cannot be represented.

    IntervalYM::from_parts

    fn IntervalYM::from_parts(negative~ : Bool, years~ : Int64, months~ : Int64) -> IntervalYM raise IntervalError

    Combine years * 12 + months, then negate when negative is true. Parts may exceed their usual calendar ranges. Raise IntervalError::Overflow if the signed total or its magnitude does not fit Int64.

    IntervalYM::is_negative

    fn IntervalYM::is_negative(self : IntervalYM) -> Bool

    Whether the signed month count is below zero. Zero is never negative.

    IntervalYM::months_part

    fn IntervalYM::months_part(self : IntervalYM) -> Int64

    Remaining months in the absolute magnitude, in 0..11.

    IntervalYM::years

    fn IntervalYM::years(self : IntervalYM) -> Int64

    Whole years in the absolute magnitude of the interval.

    MapType

    pub(all) enum MapType {
    Dynamic
    Bool
    TinyInt
    SmallInt
    Integer
    BigInt
    Numeric
    Decimal
    Real
    Double
    String
    Char
    Varchar
    Date
    Time
    Timestamp
    } derive(Eq,
    Debug
    )

    The declared key or value type of a MAP (RFC 0104). The names are the static type names of partiql-tests' $map::<key>::<value>:: encoding.

    MapType::admits

    fn MapType::admits(self : MapType, key : Value) -> Bool

    Whether a map with this key type can hold key (RFC 0104): the key is of the same type family. Every number fits a numeric key type, and every string a character key type. DYNAMIC holds no key (RFC 0104 leaves it for future work), and NULL, MISSING and non-scalar values are never keys.

    MapType::from_name

    fn MapType::from_name(name : String) -> MapType?

    The type for a name; int is accepted for integer.

    MapType::name

    fn MapType::name(self : MapType) -> String

    The type name as partiql-tests writes it.

    MapValue

    pub struct MapValue {
    key_type : MapType
    value_type : MapType
    // private fields
    }

    A PartiQL MAP: unique scalar keys, no MISSING values. The stored order is the insertion order (for output); equality and ordering ignore it.
    impl Eq for MapValue
    impl ToValue for MapValue

    MapValue::MapValue

    #alias(new, deprecated="Use MapValue(...) instead")
    fn MapValue::MapValue(key_type~ : MapType, value_type~ : MapType, entries : Array[(Value, Value)]) -> MapValue raise MapError

    Builds a MAP. Entries whose value is MISSING are dropped (RFC 0104). Keys are lowered first; NULL, MISSING and non-scalar keys, and keys equal under eqg to an earlier key, raise.

    MapValue::entries

    fn MapValue::entries(self : MapValue) -> ArrayView[(Value, Value)]

    A read-only view of the retained entries in insertion order. Keys are lowered and entries with MISSING values have already been removed.

    MapValue::length

    fn MapValue::length(self : MapValue) -> Int

    The number of retained entries after dropping MISSING values.

    NullOrder

    pub(all) enum NullOrder {
    NullsFirst
    NullsLast
    } derive(Eq,
    Debug
    )

    Where NULL and MISSING sort.

    Time

    pub struct Time {
    hour : Int
    minute : Int
    second : Int
    nanos : Int
    precision : Int
    offset : Int?
    } derive(
    Debug
    )

    A time of day. offset is minutes east of UTC; None means WITHOUT TIME ZONE. precision is the number of fractional-second digits (TIME(p)).
    impl Eq for Time
    impl ToValue for Time

    Time::Time

    #alias(new, deprecated="Use Time(...) instead")
    fn Time::Time(hour~ : Int, minute~ : Int, second~ : Int, nanos? : Int, precision? : Int, offset? : Int?) -> Time raise DateTimeError

    Build a time with hours 0..23, minutes and seconds 0..59, and fractional precision 0..9. nanos must fit that precision exactly. offset=None means WITHOUT TIME ZONE; Some(n) is minutes east of UTC in -1439..1439. Raise DateTimeError::InvalidTime for invalid fields. Leap seconds are not accepted.

    Timestamp

    pub struct Timestamp {
    date : Date
    time : Time
    } derive(
    Debug
    )

    A date and a time. The time's offset is the timestamp's offset.
    impl Eq for Timestamp

    Timestamp::Timestamp

    #alias(new, deprecated="Use Timestamp(...) instead")
    fn Timestamp::Timestamp(date : Date, time : Time) -> Timestamp

    Combine an already validated date and time. Preserve the time's precision and optional UTC offset; perform no time zone conversion.

    Tuple

    pub struct Tuple {
    // private fields
    }

    A PartiQL tuple: (name, value) pairs. Order is kept for output only; duplicate names are allowed; no value is MISSING.
    impl Eq for Tuple
    impl ToValue for Tuple

    Tuple::Tuple

    #alias(new, deprecated="Use Tuple(...) instead")
    fn Tuple::Tuple(fields : Array[(String, Value)]) -> Tuple

    Builds a tuple. Pairs whose value is MISSING are dropped.

    test {
    let tuple : @value.Tuple = @value.Tuple([
    ("a", @value.Value::Missing),
    ("b", @value.Value(2)),
    ])
    @expect.expect(tuple.get_all("b")).to_equal([@value.Value(2)])
    }

    Tuple::fields

    fn Tuple::fields(self : Tuple) -> ArrayView[(String, Value)]

    A read-only view of the retained fields, in input order. Duplicate names remain.

    Tuple::get_all

    fn Tuple::get_all(self : Tuple, name : String) -> Array[Value]

    The values of the attributes named name (case-sensitive).

    Tuple::length

    fn Tuple::length(self : Tuple) -> Int

    The number of retained name-value pairs, counting duplicate names.

    Value

    pub(all) enum Value {
    Null
    Missing
    Bool(Bool)
    Int(Int64)
    BigInt(
    BigInt
    )
    Float(Double)
    Decimal(
    Decimal
    )
    String(String)
    Clob(Bytes)
    Blob(Bytes)
    Date(Date)
    Time(Time)
    Timestamp(Timestamp)
    IntervalYM(IntervalYM)
    IntervalDT(IntervalDT)
    List(Array[Value])
    Bag(Bag)
    Tuple(Tuple)
    Map(MapValue)
    Graph(GraphValue)
    Ion(
    Ion
    )
    }

    A PartiQL value.

    Ion holds an exact Ion value (from a backtick literal or $ion::). It is a representation, never a PartiQL type: every semantic acts on its lowered value (see Value::lower). Use Value(x) for ToValue conversions or typed Value::from_* methods for known payload types. Choose from_blob or from_clob explicitly for bytes; retain variants for pattern matching and direct Null/Missing values. Mutable arrays inside list values are shared; copy them when independent storage is required.
    impl Eq for Value
    impl ToValue for Value

    Value::Value

    fn[T : ToValue] Value::Value(value : T) -> Value

    Convert a MoonBit value to its PartiQL representation. Integers use Int when they fit in Int64 and BigInt otherwise. Use Null and Missing directly, and choose from_blob or from_clob explicitly for bytes. Custom domain conversions implement ToValue. Typed from_* methods also supply expected types to nested constructors. Arrays convert recursively to lists. Bag, tuple, map, graph, datetime and interval wrappers keep their kind. Ion values remain boxed, and an existing Value passes through unchanged. Bytes have no implicit conversion. Array conversion allocates a new list; other conversions do not copy nested collection storage.

    test {
    @expect.expect(@value.Value([1, 2])).to_equal(
    @value.Value::from_list([
    @value.Value::from_int(1),
    @value.Value::from_int(2),
    ]),
    )
    }

    Value::fold

    fn[A] Value::fold(self : Value, f : (Value, Array[A]) -> A raise?, prepare? : (Value) -> Value) -> A raise?

    Folds a value tree from leaves to root without using the call stack. prepare runs before descending; f receives the prepared value and its folded children in stored order: list/bag items, tuple values, alternating map keys and values, then graph node/edge payloads. Boxed Ion values are leaves unless prepare lowers them.

    Value::from_bag

    fn Value::from_bag(value : Bag) -> Value

    Wrap an existing bag without rebuilding it; retain duplicates and absent values. The concrete parameter type disambiguates a nested Bag(...) constructor.

    Value::from_bigint

    Normalize an arbitrary-precision integer: use Int when it fits Int64, otherwise BigInt. Equivalent to the legacy Value::integer factory.

    Value::from_blob

    fn Value::from_blob(value : Bytes) -> Value

    Wrap bytes as a binary large object. Distinct from from_clob. The byte storage is shared.

    Value::from_bool

    fn Value::from_bool(value : Bool) -> Value

    Wrap a boolean as a PartiQL boolean.

    Value::from_clob

    fn Value::from_clob(value : Bytes) -> Value

    Wrap bytes as a character large object. Distinct from from_blob. The byte storage is shared; this conversion does not decode text.

    Value::from_date

    fn Value::from_date(value : Date) -> Value

    Wrap a validated Gregorian date without changing its calendar fields.

    Value::from_decimal

    fn Value::from_decimal(value :
    Decimal
    ) -> Value

    Wrap an Ion decimal without changing its coefficient, exponent or scale.

    Value::from_float

    fn Value::from_float(value : Double) -> Value

    Wrap a binary64 number, retaining NaN, infinity and signed zero.

    Value::from_graph

    fn Value::from_graph(value : GraphValue) -> Value

    Wrap a validated graph without rebuilding nodes, edges or payloads.

    Value::from_int

    fn Value::from_int(value : Int64) -> Value

    Wrap an Int64 as a PartiQL integer without changing its value.

    Value::from_interval_dt

    fn Value::from_interval_dt(value : IntervalDT) -> Value

    Wrap a normalized day-time interval without changing its seconds or nanoseconds.

    Value::from_interval_ym

    fn Value::from_interval_ym(value : IntervalYM) -> Value

    Wrap a normalized year-month interval without changing its signed month count.

    Value::from_ion

    fn Value::from_ion(value :
    Ion
    ) -> Value

    Box exact Ion, retaining annotations, symbols and representation details. This does not lower Ion or decode PartiQL annotations; use the Ion codec package's from_ion to decode PartiQL-encoded Ion instead.

    Value::from_list

    fn Value::from_list(value : Array[Value]) -> Value

    Wrap an ordered list, retaining NULL, MISSING and duplicate elements. The backing array is shared. Copy it first if independent storage is required. Unlike generic Value(array), this conversion does not map or copy elements.

    Value::from_map

    fn Value::from_map(value : MapValue) -> Value

    Wrap a validated map without rebuilding entries or changing declared types.

    Value::from_string

    fn Value::from_string(value : String) -> Value

    Wrap Unicode text as a PartiQL string.

    Value::from_time

    fn Value::from_time(value : Time) -> Value

    Wrap a validated time, retaining fractional precision and optional UTC offset.

    Value::from_timestamp

    fn Value::from_timestamp(value : Timestamp) -> Value

    Wrap a validated timestamp, retaining its date, time, precision and offset.

    Value::from_tuple

    fn Value::from_tuple(tuple : Tuple) -> Value

    Wrap an existing tuple as a PartiQL value, preserving its fields and duplicates. This is equivalent to Value(tuple) and does not rebuild the tuple. The concrete parameter type disambiguates a nested Tuple(...) constructor.

    test {
    let value = @value.Value::from_tuple(@value.Tuple([("a", @value.Value(1))]))
    guard value is @value.Value::Tuple(tuple) else {
    fail("expected a tuple value")
    }
    @expect.expect(tuple.get_all("a")).to_equal([@value.Value(1)])
    }

    Value::integer

    An integer value: Int when it fits in Int64, else BigInt.

    Value::is_absent

    fn Value::is_absent(self : Value) -> Bool

    Whether the variant is Null or Missing, without lowering boxed Ion.

    Value::lower

    fn Value::lower(self : Value) -> Value raise LowerError

    Converts a boxed Ion value to its core value, one level deep. Other values are returned unchanged.

    Value::lower_deep

    fn Value::lower_deep(self : Value) -> Value raise LowerError

    Lowers recursively, including the children of collections and tuples.

    compare

    fn compare(a : Value, b : Value, nulls~ : NullOrder) -> Int

    The ORDER BY total order (spec section 4.4). Returns -1, 0 or 1. Values are canonicalized bottom-up, then compared with an explicit stack.

    eqg

    fn eqg(a : Value, b : Value) -> Bool

    Grouping equivalence: NULL eqg NULL, MISSING eqg MISSING, NaN eqg NaN; otherwise the same as =.

    equivalent_by

    fn equivalent_by(a : Value, b : Value, decide : (Value, Value) -> Bool?) -> Bool

    The callback can decide a node comparison after shallow lowering. Returning None uses grouping equivalence for scalars and descends into collections, maps and graph payloads. It must define an equivalence relation, because unordered collections use one-to-one greedy matching.

    group_key

    fn group_key(v : Value) -> String

    A key that values which are eqg share, for hashing (the DISTINCT of SELECT, GROUP BY). It reads two levels into a collection, deeper values by kind and size only, so it takes no deep recursion; values that differ only below that share a key.

    sql_equals

    fn sql_equals(a : Value, b : Value) -> Value

    The PartiQL = operator: MISSING if either side is MISSING, else NULL if either side is NULL, else a Bool. Never fails on mixed types.

    timestamp_of_ion

    Converts an Ion timestamp: missing fields become 0, an unknown offset becomes UTC, and more than 9 fractional digits are truncated.