The PartiQL data model for MoonBit, with a PartiQL-encoded Ion codec.
Dependencies
moon add moonrockz/partiql-valueimport {
"moonrockz/partiql-value" @value,
"moonrockz/partiql-value/ion" @partiql_ion,
"moonrockz/ion/ion" @ion_core,
}import { "moonrockz/expect" } for "test"| Package | Content |
|---|---|
| moonrockz/partiql-value | Value, collections, maps, graphs, datetime and interval types, and value relations |
| moonrockz/partiql-value/ion | Single-value codecs, text/binary datagrams and Ion Schema validation |
| moonrockz/partiql-value/arbitrary | Quickcheck generators and shrinkable wrappers |
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)]))
}| Payload | Conversion |
|---|---|
| Bool, Int64, BigInt | from_bool, from_int, from_bigint |
| Double, Ion Decimal, String | from_float, from_decimal, from_string |
| Bytes | from_blob or from_clob, chosen explicitly |
| Date, Time, Timestamp | from_date, from_time, from_timestamp |
| IntervalYM, IntervalDT | from_interval_ym, from_interval_dt |
| Array[Value], Bag, Tuple | from_list, from_bag, from_tuple |
| MapValue, GraphValue, Ion IonValue | from_map, from_graph, from_ion |
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()
})
}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)])
}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))
}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))
}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))
}| Relation | Meaning |
|---|---|
| == via Eq | Structural identity: same variant and contents; bags and tuples as multisets; decimal scale counts |
| sql_equals | SQL equality over values: MISSING takes precedence over NULL; numbers compare across types |
| eqg | Grouping equivalence: NULL equals NULL, MISSING equals MISSING, NaN equals NaN |
| compare | ORDER BY total order, choosing NullsFirst or NullsLast |
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))
}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)
}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)
}pub struct Bag {
// private fields
}#alias(new, deprecated="Use Date(...) instead")
fn Date::Date(year~ : Int, month~ : Int, day~ : Int) -> Date raise DateTimeErrortest {
let leap_day : @value.Date = @value.Date(year=2024, month=2, day=29)
@expect.expect(leap_day.day).to_equal(29)
}pub(all) enum Ends {
Directed(from~ : String, to~ : String)
Undirected(String, String)
} derive(Debug)pub struct GraphValue {
// private fields
}impl Eq for GraphValueimpl Debug for GraphValueimpl ToValue for GraphValue#alias(new, deprecated="Use GraphValue(...) instead")
fn GraphValue::GraphValue(nodes : Array[GraphNode], edges : Array[GraphEdge]) -> GraphValue raise GraphErrorimpl Eq for IntervalDTimpl ToValue for IntervalDT#alias(new, deprecated="Use IntervalDT(...) instead")
fn IntervalDT::IntervalDT(seconds : Int64, nanos : Int64) -> IntervalDT raise IntervalErrorfn IntervalDT::from_parts(negative~ : Bool, days~ : Int64, hours~ : Int64, minutes~ : Int64, seconds~ : Int64, nanos~ : Int64) -> IntervalDT raise IntervalErrorimpl Eq for IntervalYMimpl ToValue for IntervalYM#alias(new, deprecated="Use IntervalYM(...) instead")
fn IntervalYM::IntervalYM(months : Int64) -> IntervalYM raise IntervalErrorfn IntervalYM::from_parts(negative~ : Bool, years~ : Int64, months~ : Int64) -> IntervalYM raise IntervalErrorpub struct Time {
hour : Int
minute : Int
second : Int
nanos : Int
precision : Int
offset : Int?
} derive(Debug)#alias(new, deprecated="Use Time(...) instead")
fn Time::Time(hour~ : Int, minute~ : Int, second~ : Int, nanos? : Int, precision? : Int, offset? : Int?) -> Time raise DateTimeErrorpub struct Tuple {
// private fields
}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)])
}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)
}test {
@expect.expect(@value.Value([1, 2])).to_equal(
@value.Value::from_list([
@value.Value::from_int(1),
@value.Value::from_int(2),
]),
)
}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)])
}Install
Download zipThe PartiQL data model for MoonBit, with a PartiQL-encoded Ion codec.
Dependencies