// Traits and generic helpers for PostgreSQL value encoding and decoding.
///|
/// PostgreSQL wire-format selection for parameters and result columns.
pub(all) enum WireFormat {
Text
Binary
} derive(Debug, Eq)
///|
fn encode_wire_format(format : WireFormat) -> Int {
match format {
Text => 0
Binary => 1
}
}
///|
fn decode_wire_format(code : Int) -> WireFormat raise {
match code {
0 => Text
1 => Binary
_ =>
raise ClientError::Protocol(
"unsupported wire format code: \{code.to_string()}",
)
}
}
///|
/// Trait implemented by values that can be encoded as query parameters.
///
/// The driver asks three questions before sending a parameter:
/// 1. `accepts`: does the codec support the PostgreSQL target type?
/// 2. `format`: should PostgreSQL receive `WireFormat::Text` or
/// `WireFormat::Binary` data?
/// 3. `to_sql`: write the actual payload or mark the value as `NULL`.
///
/// The compatibility check happens before serialization, which keeps failures
/// deterministic and improves error messages for callers.
/// A minimal custom text codec usually overrides `format` to return
/// `WireFormat::Text`,
/// checks the target OID in `accepts`, writes UTF-8 bytes in `to_sql`, and
/// returns `IsNull::No`.
pub(open) trait ToSql {
/// Return the PostgreSQL wire format to use for this value.
fn format(Self, Type) -> WireFormat = _
/// Return whether this value can be encoded for the provided PostgreSQL type.
fn accepts(Self, Type) -> Bool
/// Human-readable type name used in `WrongTypeError`.
fn moonbit_type_name(Self) -> String
/// Serialize the value into `buf`, or return `IsNull::Yes` for `NULL`.
fn to_sql(Self, Type, @buffer.Buffer) -> @proto.IsNull raise Error
}
///|
/// Default `ToSql` implementations use PostgreSQL binary format.
impl ToSql with fn format(_, _) {
Binary
}
///|
/// Trait implemented by values that can be decoded from a result column.
///
/// `Row::get` performs the `accepts` check before invoking `from_sql`, so a
/// decoder can assume it only receives compatible PostgreSQL types unless the
/// row was constructed manually in tests. For nullable columns, either decode
/// into `T?` or implement `from_sql_null` directly.
pub(open) trait FromSql {
/// Decode a non-NULL value.
fn from_sql(Type, WireFormat, BytesView) -> Self raise Error
/// Return whether this decoder accepts the PostgreSQL column type.
fn accepts(Type) -> Bool
/// Human-readable type name used in `WrongTypeError`.
fn moonbit_type_name() -> String
/// Decode a `NULL` value. The default implementation rejects `NULL`.
fn from_sql_null(Type, WireFormat) -> Self raise Error = _
}
///|
/// The default NULL decoder rejects nullable database values.
impl FromSql with fn from_sql_null(type_, _) {
raise ClientError::Decode("unexpected NULL for type \{type_.name}")
}
///|
/// Optional parameters encode `None` as SQL `NULL` and delegate `Some` values.
pub impl[T : ToSql] ToSql for T? with fn format(self, type_) {
match self {
None => Binary
Some(value) => value.format(type_)
}
}
///|
/// Optional parameters accept every PostgreSQL type when the value is `None`.
pub impl[T : ToSql] ToSql for T? with fn accepts(self, type_) {
match self {
None => true
Some(value) => value.accepts(type_)
}
}
///|
/// Optional parameters include the wrapped type name in diagnostics when present.
pub impl[T : ToSql] ToSql for T? with fn moonbit_type_name(self) {
match self {
None => "Option"
Some(value) => "\{value.moonbit_type_name()}?"
}
}
///|
/// Optional parameters serialize `None` as SQL `NULL`.
pub impl[T : ToSql] ToSql for T? with fn to_sql(self, type_, buf) {
match self {
None => Yes
Some(value) => value.to_sql(type_, buf)
}
}
///|
/// Optional decoders wrap successful non-NULL decoding in `Some`.
pub impl[T : FromSql] FromSql for T? with fn from_sql(type_, format, raw) {
let value : T = FromSql::from_sql(type_, format, raw)
Some(value)
}
///|
/// Optional decoders accept the same PostgreSQL types as their wrapped decoder.
pub impl[T : FromSql] FromSql for T? with fn accepts(type_) {
T::accepts(type_)
}
///|
/// Optional decoders annotate the wrapped type name in diagnostics.
pub impl[T : FromSql] FromSql for T? with fn moonbit_type_name() {
"\{T::moonbit_type_name()}?"
}
///|
/// Optional decoders map SQL `NULL` to `None`.
pub impl[T : FromSql] FromSql for T? with fn from_sql_null(_, _) {
None
}
///|
/// Built-in arrays use PostgreSQL's binary array payload and delegate element
/// encoding to the existing scalar codecs.
pub impl[T : ToSql] ToSql for Array[T] with fn format(_, _) {
Binary
}
///|
pub impl[T : ToSql] ToSql for Array[T] with fn accepts(self, type_) {
match array_element_type(type_) {
None => false
Some(element_type) => self.all(element => element.accepts(element_type))
}
}
///|
pub impl[T : ToSql] ToSql for Array[T] with fn moonbit_type_name(self) {
if self.length() == 0 {
"Array"
} else {
"Array[\{self[0].moonbit_type_name()}]"
}
}
///|
pub impl[T : ToSql] ToSql for Array[T] with fn to_sql(self, type_, buf) {
guard array_element_type(type_) is Some(element_type) else {
raise ClientError::Encode("cannot encode Array for type \{type_.name}")
}
let dimensions = if self.length() == 0 {
[]
} else {
[@types.ArrayDimension::new(self.length(), 1)]
}
write_array_payload(dimensions, element_type.oid, self, element_type, buf)
No
}
///|
pub impl[T : FromSql] FromSql for Array[T] with fn accepts(type_) {
match array_element_type(type_) {
None => false
Some(element_type) => T::accepts(element_type)
}
}
///|
pub impl[T : FromSql] FromSql for Array[T] with fn moonbit_type_name() {
"Array[\{T::moonbit_type_name()}]"
}
///|
pub impl[T : FromSql] FromSql for Array[T] with fn from_sql(type_, format, raw) {
guard array_element_type(type_) is Some(element_type) else {
raise ClientError::Decode("cannot decode Array from \{type_.name}")
}
match format {
Text => raise ClientError::Decode("text array decoding is not supported")
Binary => {
let pg_array = @types.array_from_sql(raw)
if pg_array.dimensions > 1 {
raise ClientError::Decode("multidimensional arrays are not supported")
}
let values : Array[T] = []
let array_values = pg_array.values()
for value = array_values.next() {
match value {
None => break values
Some(None) => {
values.push(T::from_sql_null(element_type, Binary))
continue array_values.next()
}
Some(Some(payload)) => {
values.push(T::from_sql(element_type, Binary, payload[:]))
continue array_values.next()
}
}
}
}
}
}
///|
fn[T : ToSql] array_element_to_sql(
element : T,
element_type : Type,
buf : @buffer.Buffer,
) -> @proto.IsNull raise Error {
guard element.accepts(element_type) else {
raise wrong_type_error(element.moonbit_type_name(), element_type)
}
match element_type.oid {
JSONB_OID => jsonb_array_element_to_sql(element, element_type, buf)
_ => element.to_sql(element_type, buf)
}
}
///|
fn[T : ToSql] jsonb_array_element_to_sql(
element : T,
element_type : Type,
buf : @buffer.Buffer,
) -> @proto.IsNull raise Error {
let payload = Buffer()
match element.to_sql(element_type, payload) {
Yes => Yes
No => {
buf.write_byte(b'\x01')
buf.write_bytes(payload.to_bytes())
No
}
}
}
///|
fn[T : ToSql] write_array_payload(
dimensions : Array[@types.ArrayDimension],
element_oid : @proto.Oid,
elements : Array[T],
element_type : Type,
buf : @buffer.Buffer,
) -> Unit raise Error {
let payload = Buffer()
let mut expected_elements = 1
for dimension in dimensions {
if dimension.len < 0 {
raise ClientError::Encode("invalid array dimension size")
}
if dimension.len != 0 {
let max = 0x7fff_ffff / dimension.len
if expected_elements > max {
raise ClientError::Encode("too many array elements")
}
}
expected_elements = expected_elements * dimension.len
payload.write_int_be(@proto.checked_i32(dimension.len))
payload.write_int_be(@proto.checked_i32(dimension.lower_bound))
}
if dimensions.length() == 0 {
expected_elements = 0
}
if elements.length() != expected_elements {
raise ClientError::Encode("array element count does not match dimensions")
}
let mut has_nulls = false
for element in elements {
let element_payload = Buffer()
let is_null = array_element_to_sql(element, element_type, element_payload)
match is_null {
Yes => {
has_nulls = true
payload.write_int_be(-1)
}
No => {
payload.write_int_be(@proto.i32_from_usize(element_payload.length()))
payload.write_bytes(element_payload.to_bytes())
}
}
}
buf.write_int_be(@proto.i32_from_usize(dimensions.length()))
buf.write_int_be(if has_nulls { 1 } else { 0 })
buf.write_uint_be(element_oid)
buf.write_bytes(payload.to_bytes())
}