Sign in

    partiql-eval

    A PartiQL evaluator for MoonBit.

    partiql
    sql
    query
    evaluator
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    12 hours ago
    Downloads
    4

    #moonrockz/partiql-eval

    A PartiQL evaluator for MoonBit. It lowers a syntax tree to a logical plan and interprets the plan against global bindings. It runs in permissive mode (the default) or in strict mode.

    #Installation and imports

    moon add moonrockz/partiql-eval

    Add these imports to moon.pkg for the examples below:

    import {
    "moonrockz/partiql-eval" @eval,
    "moonrockz/partiql-value" @value,
    "moonrockz/partiql-value/ion" @partiql_ion,
    "moonrockz/ion/ion" @ion,
    "moonbitlang/x/time",
    }

    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"

    #Expressions and queries

    test {
    let value = @eval.evaluate_text("1 + 2", env=@eval.Bindings::empty())
    @expect.expect(value).to_equal(@value.Value::from_int(3))
    }

    A query gives a bag. This one has no global bindings:

    test {
    let value = @eval.evaluate_text(
    "SELECT VALUE x * 10 FROM [1, 2, 3] AS x WHERE x > 1",
    env=@eval.Bindings::empty(),
    )
    @expect.expect(value == @value.Value::from_bag(@value.Bag([@value.Value::from_int(20), @value.Value::from_int(30)]))).to_be_true()
    }

    ORDER BY gives a list, and LIMIT and OFFSET take a window of it:

    test {
    let value = @eval.evaluate_text(
    "SELECT VALUE x FROM [3, 1, 2] AS x ORDER BY x DESC LIMIT 2",
    env=@eval.Bindings::empty(),
    )
    @expect.expect(value == @value.Value::from_list([@value.Value::from_int(3), @value.Value::from_int(2)])).to_be_true()
    }

    GROUP BY makes one row per group, and the aggregates fold each group:

    test {
    let value = @eval.evaluate_text(
    "SELECT r, SUM(s.n) AS total FROM [{'r': 'a', 'n': 1}, {'r': 'a', 'n': 2}] AS s GROUP BY s.r AS r",
    env=@eval.Bindings::empty(),
    )
    @expect.expect(value ==
    @value.Value::from_bag(
    @value.Bag([
    @value.Value::from_tuple(@value.Tuple([("r", @value.Value::from_string("a")), ("total", @value.Value::from_int(3))])),
    ]),
    )).to_be_true()
    }

    The built-in functions count Unicode code points and use full case mapping:

    test {
    let value = @eval.evaluate_text(
    "UPPER(TRIM(' straße '))",
    env=@eval.Bindings::empty(),
    )
    @expect.expect(value == @value.Value::from_string("STRASSE")).to_be_true()
    }

    CAST converts a value to a type. A text that is not a valid literal of the type gives MISSING (a TypeError in strict mode); a value out of the range of the type raises a DataError in both modes:

    test {
    let value = @eval.evaluate_text(
    "CAST('3.14159' AS DECIMAL(4, 2))",
    env=@eval.Bindings::empty(),
    )
    @expect.expect(value == @value.Value::from_decimal(@ion.Decimal::parse("3.14"))).to_be_true()
    }

    The datetime functions follow SQL where it defines them (EXTRACT, OVERLAPS) and the PartiQL format patterns for TO_STRING and TO_TIMESTAMP:

    test {
    let value = @eval.evaluate_text(
    "TO_STRING(DATE_ADD(day, 30, DATE '2024-02-15'), 'MMMM d, yyyy')",
    env=@eval.Bindings::empty(),
    )
    @expect.expect(value == @value.Value::from_string("March 16, 2024")).to_be_true()
    }

    The clock functions (CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, UTCNOW) read a clock that you inject; the library never reads the wall clock:

    test {
    let clock = @eval.Clock::fixed(@time.date_time(2026, 10, 16, hour=9))
    let value = @eval.evaluate_text(
    "CURRENT_DATE",
    env=@eval.Bindings::empty(),
    clock~,
    )
    @expect.expect(value == @value.Value::from_date(@value.Date(year=2026, month=10, day=16))).to_be_true()
    }

    Pass mode=@eval.Mode::Strict to make a type error raise instead of giving MISSING.

    #What it evaluates

    This version evaluates expressions: literals, collections, paths, arithmetic, comparison, logic, LIKE, BETWEEN, IN, IS, CASE, COALESCE, NULLIF, and datetime and interval arithmetic. It evaluates the core of a query: FROM (collections, AT, UNPIVOT, joins), LET, WHERE, SELECT VALUE, SELECT lists, SELECT *, DISTINCT, subqueries and VALUES. It also evaluates ORDER BY, LIMIT and OFFSET, the set operations (UNION, INTERSECT and EXCEPT, with ALL, OUTER and CORRESPONDING), PIVOT, TABLE and WITH, and grouping: GROUP BY, GROUP AS, GROUP ALL, HAVING, the SQL aggregates (COUNT, SUM, AVG, MIN, MAX, ANY, SOME, EVERY) and the COLL_ functions. It evaluates the built-in functions CHAR_LENGTH, OCTET_LENGTH, BIT_LENGTH, UPPER, LOWER, TRIM, SUBSTRING, POSITION, OVERLAY, TEXT_REPLACE, ABS, MOD, SIZE, CARDINALITY, EXISTS, TUPLEUNION and the MAP_ functions. It evaluates CAST to every type except SEXP and ARRAY<T>; evaluate takes the session time zone of CAST as zone~ (default UTC). It evaluates the datetime functions EXTRACT, DATE_ADD, DATE_DIFF, OVERLAPS, TO_STRING and TO_TIMESTAMP, and the clock functions CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP and UTCNOW (with an injected clock~). A form that is not supported yet raises EvalError::Unsupported, never a wrong answer.

    #Errors

    • TypeError: an operand has the wrong type. Permissive mode gives MISSING; strict mode raises.
    • DataError: the data is wrong (division by zero, Int64 overflow, an undefined variable), or the statement is wrong before any data is read (an unknown function name, a CAST to NULL). It raises in both modes.
    • Unsupported: the form is not evaluated yet.
    • Syntax: evaluate_text could not parse the text.

    #Ion sessions

    Import moonrockz/partiql-value/ion as @partiql_ion. An Ion datagram is an ordered array of top-level values. The readers accept text or bytes; read_bytes detects binary Ion by its version marker and otherwise reads strict UTF-8 text. IonDataError keeps text, binary, encoding, schema and PartiQL codec failures distinct.

    This example reads a binary environment, evaluates a script and writes binary results:

    test {
    let input = @partiql_ion.write_bytes(@partiql_ion.read_text("{a: 2, b: $bag::[1, 3]}"))
    let session = @eval.Session::Session(env=@eval.Bindings::from_ion_bytes(input))
    let output = session.evaluate_ion_bytes("a + 1; SELECT VALUE x * a FROM b AS x")
    @expect.expect(@partiql_ion.read_bytes(output)).to_equal(@partiql_ion.read_text("3 $bag::[2, 6]"))
    }

    Bindings::from_ion_text and from_ion_bytes require exactly one struct. For named data, use Bindings::empty().with_data("t", values): no elements bind an empty bag, one binds as-is, and many bind a bag. For document-by-document execution, session.evaluate_each("t.a", name="t", values~) returns one result per element, in order. Empty input returns no results. Session bindings are unchanged, including after a failure.

    Session(...) accepts env~, mode~, zone~ and clock~ with the same defaults as evaluate. evaluate_script parses the whole script before running any statement. evaluate_ion_text and evaluate_ion_bytes return one datagram for all script results, or raise SessionError::Evaluation or SessionError::Ion. Statements stop at the first error. Sessions keep no partial results; the CLI retains results preceding a failing statement.

    For schema validation, pass validation~ to an Ion reader or binding constructor. Build it with Validation::elements(schema, "row") for per-element validation or Validation::document(schema, "rows") for a whole-datagram schema. Validation checks raw Ion before PartiQL decoding, collects every violation and retains its path. A schema must describe the physical PartiQL encoding, including annotations such as $bag and $missing. Readers also accept catalog~ for shared symbol imports.

    #Package boundaries and reuse

    PackagePurpose
    moonrockz/partiql-evalevaluate, evaluate_text, Bindings, Session; re-exported mode, errors, clock and zone types
    moonrockz/partiql-eval/planLowered expression and relation ADTs with source spans
    moonrockz/partiql-eval/lowerAST-to-plan validation and name resolution
    moonrockz/partiql-eval/opsPure value operators, casts, functions and clock abstraction
    moonrockz/partiql-eval/interpPlan execution with a fresh context per statement

    Use the root package for ordinary evaluation. Parse once with @syntax.parse, then call evaluate(statement, env~) for different bindings. Lower and interpret plans directly only when your application needs to manage those steps. Operator functions raise OpFailure; the interpreter attaches source spans and applies permissive/strict mode.

    Bindings::of_pairs copies its input and preserves MISSING bindings and repeated names. Bindings::of_tuple uses the tuple's retained fields. Unquoted lookup ignores ASCII case; quoted names match exactly. Duplicate matches raise an ambiguity error when referenced. with_value returns a new environment in which the exact name replaces earlier bindings of that name.

    test "reusable session" {
    let env = @eval.Bindings::of_pairs([("x", @value.Value(4))])
    let session = @eval.Session(env~, mode=@eval.Mode::Strict)
    @expect.expect(session.evaluate_script("x + 1; x * 2")).to_equal([@value.Value(5), @value.Value(8)])
    }

    Construct sessions with Session(...); Session::new is a deprecated alias. ZoneOffset::from_minutes returns None for offsets outside -18:00..+18:00; ZoneOffset::utc() supplies UTC. Clock reads can fail and are translated to DataError. Each statement samples its clock on first use and reuses that instant for every clock function within the statement.

    All checked examples run through mise run test:docs. See the repository's architecture and conformance report for supported syntax and behavior.

    Clock

    Where "now" comes from. The library never reads a wall clock: the composition root (the CLI's main) builds the real clock, and tests and embedders pass fixed or fake ones. now may raise; the evaluator turns a raise into a data error.

    EvalError

    An evaluation failure with a source location, or the original parse error. TypeError is a strict-mode operand failure; DataError is invalid data or a static error in either mode; Unsupported is an unimplemented form.

    Mode

    How the interpreter handles runtime type failures. Permissive mode yields MISSING; strict mode raises TypeError. Data and unsupported-form failures raise in either mode.

    ZoneOffset

    The session time zone displacement in minutes east of UTC, −18:00 to +18:00 (spec M4d2 §4.4).

    SessionError

    Either evaluation or output encoding failed at the session boundary.

    Bindings

    pub struct Bindings {
    // private fields
    }

    The global bindings of an evaluation: names to values.

    Bindings::empty

    fn Bindings::empty() -> Bindings

    An environment with no global names.

    Bindings::from_ion_bytes

    Decode a binary or UTF-8 text environment, detected from the version marker.

    Bindings::from_ion_text

    Decode exactly one environment struct. Catalog and validation apply to raw Ion.

    Bindings::of_ion

    The bindings of an Ion struct, read as PartiQL-encoded Ion. A value that is not a struct is a DataError.

    Bindings::of_pairs

    fn Bindings::of_pairs(entries : Array[(String,
    Value
    )]) -> Bindings

    The bindings entries, in order. Unlike of_tuple, a global whose value is MISSING stays bound.

    Bindings::of_tuple

    Bind each retained tuple field, preserving order and duplicate names. Tuple construction has already removed MISSING fields. Ambiguous names are reported when evaluation resolves them.

    Bindings::with_data

    fn Bindings::with_data(self : Bindings, name : String, values : Array[
    Value
    ]) -> Bindings

    Bind a whole datagram: zero elements form an empty bag, one is used as-is, and many form a bag. Use Session::evaluate_each for individual documents.

    Bindings::with_value

    fn Bindings::with_value(self : Bindings, name : String, value :
    Value
    ) -> Bindings

    Return bindings with name replaced or appended. The input array is copied.

    Session

    Reusable evaluation settings. Evaluation never replaces session bindings.

    Session::Session

    #alias(new, deprecated="Use Session(...) instead")
    fn Session::Session(env? : Bindings, mode? :
    Mode
    , zone? :
    ZoneOffset
    , clock? :
    Clock
    ) -> Session

    Build reusable evaluation settings. Default to empty globals, permissive mode and UTC. Without clock, clock functions raise an evaluation error. Each evaluated statement samples the clock at most once.

    test {
    let session = @eval.Session()
    @expect.expect(session.evaluate_script("1 + 2; 3 * 4")).to_equal([
    @value.Value(3),
    @value.Value(12),
    ])
    }

    Session::evaluate

    Parse and evaluate one statement with this session's settings. Raise EvalError::Syntax for invalid text or an evaluation error on failure. Session bindings remain unchanged.

    Session::evaluate_each

    Parse once, bind each top-level element to name, and evaluate in order. Empty input yields no results. A failure stops evaluation at that element.

    Session::evaluate_ion_bytes

    fn Session::evaluate_ion_bytes(self : Session, source : String) -> Bytes raise SessionError

    Evaluate a complete script and encode its results as one binary Ion datagram. Raise SessionError on evaluation or encoding failure; no partial output is returned.

    Session::evaluate_ion_text

    fn Session::evaluate_ion_text(self : Session, source : String, pretty? : Bool) -> String raise SessionError

    Evaluate a complete script and encode its results as PartiQL Ion text, one value per line. pretty controls each value's layout. Raise SessionError on evaluation or encoding failure; no partial output is returned. Empty scripts produce an empty string.

    Session::evaluate_script

    Parse the complete script before evaluation, then return results in order.

    evaluate

    Evaluates a statement. A form that this version does not evaluate raises Unsupported. zone is the session time zone of CAST (default UTC); clock gives the statement time of the clock functions (default none: they fail).

    evaluate_text

    Parses and evaluates a statement. A syntax error raises Syntax.

    test {
    let env = @eval.Bindings::of_pairs([("x", @value.Value(7))])
    @expect.expect(@eval.evaluate_text("x + 1", env~)).to_equal(@value.Value(8))
    }