///|
/// Which columns to read, and how to turn the values back into one MoonBit
/// value.
///
/// Kept as one type rather than two because apart they break the usual way: a
/// column is added to the projection and the decoder is not updated to match.
/// `read` is positional, which is fragile by hand — but a generator emits both
/// halves from one pass over the same field list, so they cannot drift.
pub struct Selection[Out] {
exprs : Array[RawExpr]
read : (Row) -> Out raise DecodeError
}
///|
/// Build a projection from raw expressions and a positional decoder.
///
/// Intended for generated code: a generator emits `exprs` and `read` from the
/// same entity definition, so their order cannot drift apart.
pub fn[Out] Selection::new(
exprs : Array[RawExpr],
read : (Row) -> Out raise DecodeError,
) -> Selection[Out] {
{ exprs, read }
}
///|
/// Read one column, decoded as `T`.
pub fn[T : SqlDecode] sel(c : Column[T]) -> Selection[T] {
let key = c.name
Selection::new([c.raw()], row => SqlDecode::decode(row[0], key))
}
///|
/// Read two columns as a pair.
pub fn[A : SqlDecode, B : SqlDecode] sel2(
x : Column[A],
y : Column[B],
) -> Selection[(A, B)] {
sel(x).zip(sel(y))
}
///|
/// Read three columns as a triple.
pub fn[A : SqlDecode, B : SqlDecode, C : SqlDecode] sel3(
x : Column[A],
y : Column[B],
z : Column[C],
) -> Selection[(A, B, C)] {
sel(x)
.zip(sel(y))
.zip(sel(z))
.map(t => {
let ((a, b), c) = t
(a, b, c)
})
}
///|
/// Concatenate two projections, self's columns first.
pub fn[A, B] Selection::zip(
self : Selection[A],
other : Selection[B],
) -> Selection[(A, B)] {
let n = self.exprs.length()
let exprs = self.exprs.copy()
exprs.push_iter(other.exprs.iter())
Selection::new(exprs, row => ((self.read)(row[0:n]), (other.read)(row[n:])))
}
///|
/// Reshape what a projection decodes to, without touching which columns it
/// reads.
pub fn[A, B] Selection::map(
self : Selection[A],
f : (A) -> B raise DecodeError,
) -> Selection[B] {
Selection::new(self.exprs, row => f((self.read)(row)))
}
///|
/// Collapse a pair into a value of your own.
pub fn[A, B, R] Selection::into2(
self : Selection[(A, B)],
f : (A, B) -> R,
) -> Selection[R] {
self.map(t => f(t.0, t.1))
}
///|
/// Collapse a triple into a value of your own.
pub fn[A, B, C, R] Selection::into3(
self : Selection[(A, B, C)],
f : (A, B, C) -> R,
) -> Selection[R] {
self.map(t => f(t.0, t.1, t.2))
}
///|
/// Read the projection as absent when the row did not match.
///
/// A LEFT JOIN reports "no match" by making every column of the right table
/// NULL, so an all-NULL row decodes to `None` rather than being forced through
/// a decoder that would reject it.
///
/// This is safe for any projection that includes a column the database never
/// leaves NULL — a primary key, for instance, which `Table::all` always covers.
/// For a hand-built projection of entirely nullable columns, say which column
/// to test with `optional_on`.
pub fn[Out] Selection::optional(self : Selection[Out]) -> Selection[Out?] {
Selection::new(self.exprs, row => {
if row.iter().all(v => v is VNull) {
None
} else {
Some((self.read)(row))
}
})
}
///|
/// Read the projection as absent when one chosen column is NULL.
///
/// `key` indexes into this projection's own columns, counting from zero.
pub fn[Out] Selection::optional_on(
self : Selection[Out],
key~ : Int,
) -> Selection[Out?] {
Selection::new(self.exprs, row => {
if row[key] is VNull {
None
} else {
Some((self.read)(row))
}
})
}