Skip to main content

questdb/egress/
binds.rs

1/*******************************************************************************
2 *     ___                  _   ____  ____
3 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
4 *   | | | | | | |/ _ \/ __| __| | | |  _ \
5 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
6 *    \__\_\\__,_|\___||___/\__|____/|____/
7 *
8 *  Copyright (c) 2014-2019 Appsicle
9 *  Copyright (c) 2019-2025 QuestDB
10 *
11 *  Licensed under the Apache License, Version 2.0 (the "License");
12 *  you may not use this file except in compliance with the License.
13 *  You may obtain a copy of the License at
14 *
15 *  http://www.apache.org/licenses/LICENSE-2.0
16 *
17 *  Unless required by applicable law or agreed to in writing, software
18 *  distributed under the License is distributed on an "AS IS" BASIS,
19 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 *  See the License for the specific language governing permissions and
21 *  limitations under the License.
22 *
23 ******************************************************************************/
24
25//! Bind-parameter wire encoding for `QUERY_REQUEST`.
26//!
27//! Each bind serialises as a single-row column body: type code, null
28//! section, column-level type args (if any), then the per-row value(s).
29//!
30//! ```text
31//! type_code:  u8
32//! null_flag:  u8                0x00 = no bitmap; 0x01 = bitmap follows
33//! [bitmap]:   u8                present iff null_flag == 0x01; LSB-first, 1 = NULL
34//! column args:                  per type, present per the rules below:
35//!   DECIMAL64/128/256:          1 B scale (always present, including nulls)
36//!   GEOHASH:                    varint precision_bits (1..=60; always present)
37//!   VARCHAR/BINARY:             (non_null + 1) × u32_le offsets — non-null only
38//!   everything else:            (no args)
39//! values × non_null:            type-specific layout (see per-type docs below)
40//! ```
41//!
42//! Multi-byte numeric values are little-endian. For null binds,
43//! `non_null = 0`, so:
44//! - simple types emit `[type, 0x01, 0x01]`
45//! - DECIMAL\* emit `[type, 0x01, 0x01, scale]`
46//! - GEOHASH emits `[type, 0x01, 0x01, varint(precision_bits)]`
47//! - VARCHAR/BINARY emit `[type, 0x01, 0x01]` (the server's bind decoder
48//!   skips the offsets array on the null branch — emitting them would
49//!   poison the next bind in a multi-bind QUERY_REQUEST)
50
51use std::net::Ipv4Addr;
52
53use crate::egress::column_kind::ColumnKind;
54use crate::egress::wire::varint;
55use crate::error::{Result, fmt};
56
57// ============================================================================
58// PHASE 1 SERVER COMPATIBILITY — bind-type gap
59// ============================================================================
60//
61// Single source of truth for the bind types the Phase 1 server doesn't
62// accept. Every client-side rejection / encoder note in this file
63// references this block by the literal marker `PHASE 1 SERVER
64// COMPATIBILITY` so enabling a type later is one grep.
65//
66// Reference: `core/src/main/java/io/questdb/cutlass/qwp/server/egress/QwpEgressRequestDecoder.java`
67// `decodeBind` switch.
68//
69// - **BINARY (0x17), IPv4 (0x18)** — no decoder case on the server;
70//   fall into `default ->` with "unsupported wire type". Client rejects
71//   in `check_bindable` so the user sees a typed `InvalidBind` instead
72//   of an out-of-band `QUERY_ERROR` that arrives with `request_id=0`
73//   and breaks correlation.
74// - **DOUBLE_ARRAY (0x11), LONG_ARRAY (0x12)** — explicit server case
75//   throwing "ARRAY bind parameters not yet supported in Phase 1
76//   egress". The QWP spec (§6 "Bind parameters") describes the
77//   eventual array bind encoding (per-row dimension header), so this
78//   is a Phase 1 limitation that may be lifted server-side.
79// - **SYMBOL (0x09)** — defensive. The Phase 1 server currently
80//   accepts SYMBOL bind type codes leniently, dispatching them to
81//   `BindVariableService.setStr` (spec §6 "Server leniency note"). The
82//   spec instructs compliant clients to send STRING / VARCHAR for
83//   symbol binds, and a future server revision may tighten this. The
84//   Rust `Bind` enum has no `Symbol(_)` value variant and
85//   `SimpleNullKind` excludes `Symbol`, so this arm is unreachable
86//   through the typed API; it stays as a defense against any future
87//   code path that synthesises a SYMBOL-kinded `Bind`.
88//
89// Encoder arms for IPv4 / Binary remain wired for forward
90// compatibility — when the server lifts a restriction the bytes are
91// already correct and only `check_bindable` needs editing.
92// ============================================================================
93
94/// Inclusive per-width upper bounds on a DECIMAL column's scale,
95/// matching the server's `Decimal{64,128,256}.MAX_SCALE`
96/// (`io/questdb/std/Decimal*.java`: 18 / 38 / 76). Negative scales and
97/// scales above the width's bound are rejected client-side at encode
98/// time so the user gets `InvalidBind` immediately rather than a
99/// generic `QUERY_ERROR` from the server.
100pub const DECIMAL64_MAX_SCALE: i8 = 18;
101pub const DECIMAL128_MAX_SCALE: i8 = 38;
102pub const DECIMAL256_MAX_SCALE: i8 = 76;
103
104/// Column kinds whose null wire encoding is the simple no-args form
105/// `[type_code, null_flag=0x01, bitmap=0x01]` — no column-level
106/// metadata, no offsets array. Acts as the type-system constraint on
107/// [`Bind::Null`]: kinds excluded here either need extra metadata
108/// (DECIMAL\* scale, GEOHASH precision_bits) or have a different null
109/// layout (VARCHAR, BINARY) and use a dedicated `Null*` variant.
110///
111/// SYMBOL, DOUBLE_ARRAY, LONG_ARRAY are excluded — see `PHASE 1 SERVER
112/// COMPATIBILITY` block at the top of this module for the server-side
113/// rationale and the conditions under which each may be re-enabled.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115#[non_exhaustive]
116pub enum SimpleNullKind {
117    Boolean,
118    Byte,
119    Short,
120    Int,
121    Long,
122    Float,
123    Double,
124    Timestamp,
125    TimestampNanos,
126    Date,
127    Uuid,
128    Long256,
129    Char,
130    Ipv4,
131}
132
133impl SimpleNullKind {
134    /// The corresponding [`ColumnKind`].
135    pub fn as_column_kind(self) -> ColumnKind {
136        match self {
137            SimpleNullKind::Boolean => ColumnKind::Boolean,
138            SimpleNullKind::Byte => ColumnKind::Byte,
139            SimpleNullKind::Short => ColumnKind::Short,
140            SimpleNullKind::Int => ColumnKind::Int,
141            SimpleNullKind::Long => ColumnKind::Long,
142            SimpleNullKind::Float => ColumnKind::Float,
143            SimpleNullKind::Double => ColumnKind::Double,
144            SimpleNullKind::Timestamp => ColumnKind::Timestamp,
145            SimpleNullKind::TimestampNanos => ColumnKind::TimestampNanos,
146            SimpleNullKind::Date => ColumnKind::Date,
147            SimpleNullKind::Uuid => ColumnKind::Uuid,
148            SimpleNullKind::Long256 => ColumnKind::Long256,
149            SimpleNullKind::Char => ColumnKind::Char,
150            SimpleNullKind::Ipv4 => ColumnKind::Ipv4,
151        }
152    }
153}
154
155impl TryFrom<ColumnKind> for SimpleNullKind {
156    type Error = ColumnKind;
157
158    /// Returns the input kind in `Err` when it's not a simple-null kind, so
159    /// the caller can build a context-rich error message pointing at the
160    /// dedicated variant the user needed.
161    fn try_from(k: ColumnKind) -> std::result::Result<Self, Self::Error> {
162        Ok(match k {
163            ColumnKind::Boolean => SimpleNullKind::Boolean,
164            ColumnKind::Byte => SimpleNullKind::Byte,
165            ColumnKind::Short => SimpleNullKind::Short,
166            ColumnKind::Int => SimpleNullKind::Int,
167            ColumnKind::Long => SimpleNullKind::Long,
168            ColumnKind::Float => SimpleNullKind::Float,
169            ColumnKind::Double => SimpleNullKind::Double,
170            ColumnKind::Timestamp => SimpleNullKind::Timestamp,
171            ColumnKind::TimestampNanos => SimpleNullKind::TimestampNanos,
172            ColumnKind::Date => SimpleNullKind::Date,
173            ColumnKind::Uuid => SimpleNullKind::Uuid,
174            ColumnKind::Long256 => SimpleNullKind::Long256,
175            ColumnKind::Char => SimpleNullKind::Char,
176            ColumnKind::Ipv4 => SimpleNullKind::Ipv4,
177            other => return Err(other),
178        })
179    }
180}
181
182/// Typed bind value.
183///
184/// Position is implicit in the order binds are emitted into a `QUERY_REQUEST`
185/// (`$1`, `$2`, …). Types whose null wire encoding carries column-level
186/// metadata have dedicated `Null*` variants; everything else uses
187/// [`Bind::Null`].
188///
189/// `#[non_exhaustive]` so future bind types (e.g. when the server
190/// lifts the array-bind restriction documented in the `PHASE 1 SERVER
191/// COMPATIBILITY` block at module top) can be added without breaking
192/// exhaustive matches in user code.
193#[derive(Debug, Clone, PartialEq)]
194#[non_exhaustive]
195pub enum Bind {
196    // --- Simple typed-NULL (column body is just the null section) ----------
197    /// Typed NULL for any simple-null kind. The [`SimpleNullKind`] type
198    /// statically excludes kinds (VARCHAR / BINARY / DECIMAL\* / GEOHASH)
199    /// whose null wire encoding requires column-level metadata, so an
200    /// invalid `Bind::Null` is unrepresentable.
201    Null(SimpleNullKind),
202    /// Typed NULL for VARCHAR (offsets array length-1 even with no values).
203    NullVarchar,
204    /// Typed NULL for BINARY (same offsets-array reason).
205    NullBinary,
206    /// Typed NULL for DECIMAL64 (scale must be on the wire).
207    NullDecimal64 {
208        scale: i8,
209    },
210    /// Typed NULL for DECIMAL128.
211    NullDecimal128 {
212        scale: i8,
213    },
214    /// Typed NULL for DECIMAL256.
215    NullDecimal256 {
216        scale: i8,
217    },
218    /// Typed NULL for GEOHASH (precision must be on the wire).
219    NullGeohash {
220        precision_bits: u8,
221    },
222
223    // --- Value binds -------------------------------------------------------
224    Bool(bool),
225    /// Maps to QWP `BYTE` (signed 8-bit).
226    I8(i8),
227    /// Maps to QWP `SHORT` (signed 16-bit).
228    I16(i16),
229    /// Maps to QWP `INT` (signed 32-bit).
230    I32(i32),
231    /// Maps to QWP `LONG` (signed 64-bit).
232    I64(i64),
233    F32(f32),
234    F64(f64),
235    Varchar(String),
236    Binary(Vec<u8>),
237    /// QWP `TIMESTAMP` (microseconds since epoch).
238    TimestampMicros(i64),
239    /// QWP `TIMESTAMP_NANOS` (nanoseconds since epoch).
240    TimestampNanos(i64),
241    /// QWP `DATE` (milliseconds since epoch).
242    DateMillis(i64),
243    /// 16 raw bytes; high/low long ordering is the caller's responsibility.
244    Uuid([u8; 16]),
245    /// 32 raw bytes; LONG256 is opaque on the wire.
246    Long256([u8; 32]),
247    /// 2-byte UTF-16 code unit (CHAR).
248    Char(u16),
249    Ipv4(Ipv4Addr),
250    /// QWP `DECIMAL64`: i64 mantissa + scale.
251    Decimal64 {
252        value: i64,
253        scale: i8,
254    },
255    /// QWP `DECIMAL128`: i128 mantissa + scale.
256    Decimal128 {
257        value: i128,
258        scale: i8,
259    },
260    /// QWP `DECIMAL256`: 32-byte LE mantissa + scale.
261    Decimal256 {
262        bytes: [u8; 32],
263        scale: i8,
264    },
265    /// QWP `GEOHASH`: zero-extended u64 + precision_bits (1..=60). The
266    /// least-significant `ceil(precision_bits/8)` bytes are written.
267    Geohash {
268        value: u64,
269        precision_bits: u8,
270    },
271}
272
273impl Bind {
274    /// QWP type code this bind serializes to.
275    pub fn kind(&self) -> ColumnKind {
276        match self {
277            Bind::Null(s) => s.as_column_kind(),
278            Bind::NullVarchar => ColumnKind::Varchar,
279            Bind::NullBinary => ColumnKind::Binary,
280            Bind::NullDecimal64 { .. } => ColumnKind::Decimal64,
281            Bind::NullDecimal128 { .. } => ColumnKind::Decimal128,
282            Bind::NullDecimal256 { .. } => ColumnKind::Decimal256,
283            Bind::NullGeohash { .. } => ColumnKind::Geohash,
284            Bind::Bool(_) => ColumnKind::Boolean,
285            Bind::I8(_) => ColumnKind::Byte,
286            Bind::I16(_) => ColumnKind::Short,
287            Bind::I32(_) => ColumnKind::Int,
288            Bind::I64(_) => ColumnKind::Long,
289            Bind::F32(_) => ColumnKind::Float,
290            Bind::F64(_) => ColumnKind::Double,
291            Bind::Varchar(_) => ColumnKind::Varchar,
292            Bind::Binary(_) => ColumnKind::Binary,
293            Bind::TimestampMicros(_) => ColumnKind::Timestamp,
294            Bind::TimestampNanos(_) => ColumnKind::TimestampNanos,
295            Bind::DateMillis(_) => ColumnKind::Date,
296            Bind::Uuid(_) => ColumnKind::Uuid,
297            Bind::Long256(_) => ColumnKind::Long256,
298            Bind::Char(_) => ColumnKind::Char,
299            Bind::Ipv4(_) => ColumnKind::Ipv4,
300            Bind::Decimal64 { .. } => ColumnKind::Decimal64,
301            Bind::Decimal128 { .. } => ColumnKind::Decimal128,
302            Bind::Decimal256 { .. } => ColumnKind::Decimal256,
303            Bind::Geohash { .. } => ColumnKind::Geohash,
304        }
305    }
306
307    fn is_null(&self) -> bool {
308        matches!(
309            self,
310            Bind::Null(_)
311                | Bind::NullVarchar
312                | Bind::NullBinary
313                | Bind::NullDecimal64 { .. }
314                | Bind::NullDecimal128 { .. }
315                | Bind::NullDecimal256 { .. }
316                | Bind::NullGeohash { .. }
317        )
318    }
319}
320
321/// Append the wire encoding of `bind` to `out`.
322pub fn encode_bind(bind: &Bind, out: &mut Vec<u8>) -> Result<()> {
323    // `Bind::Null(SimpleNullKind)` only encodes the simple no-args null body
324    // `[type, null_flag=0x01, bitmap=0x01]`. The `SimpleNullKind` enum
325    // statically excludes kinds whose null wire encoding requires
326    // column-level metadata (DECIMAL\* scale, GEOHASH precision_bits) or
327    // whose null layout differs from a bare null section (VARCHAR /
328    // BINARY) — those route through dedicated `Null*` variants.
329    out.push(bind.kind().as_u8());
330
331    let null = bind.is_null();
332    if null {
333        out.push(0x01); // null_flag
334        out.push(0x01); // bitmap: bit 0 set -> row 0 is NULL
335    } else {
336        out.push(0x00);
337    }
338
339    // Column-level type args (always present; type-specific count of values
340    // comes after).
341    match bind {
342        // DECIMAL: column-level scale.
343        Bind::Decimal64 { scale, .. }
344        | Bind::Decimal128 { scale, .. }
345        | Bind::Decimal256 { scale, .. }
346        | Bind::NullDecimal64 { scale }
347        | Bind::NullDecimal128 { scale }
348        | Bind::NullDecimal256 { scale } => {
349            let max_scale = match bind {
350                Bind::Decimal64 { .. } | Bind::NullDecimal64 { .. } => DECIMAL64_MAX_SCALE,
351                Bind::Decimal128 { .. } | Bind::NullDecimal128 { .. } => DECIMAL128_MAX_SCALE,
352                _ => DECIMAL256_MAX_SCALE,
353            };
354            if *scale < 0 || *scale > max_scale {
355                return Err(fmt!(
356                    InvalidBind,
357                    "decimal scale {} outside 0..={}",
358                    scale,
359                    max_scale
360                ));
361            }
362            out.push(*scale as u8);
363        }
364        // GEOHASH: column-level varint precision_bits.
365        Bind::Geohash { precision_bits, .. } | Bind::NullGeohash { precision_bits } => {
366            if *precision_bits == 0 || *precision_bits > 60 {
367                return Err(fmt!(
368                    InvalidBind,
369                    "geohash precision_bits {} outside 1..=60",
370                    precision_bits
371                ));
372            }
373            if let Bind::Geohash {
374                value,
375                precision_bits,
376            } = bind
377            {
378                // `precision_bits` is in 1..=60, so the shift is always
379                // well-defined; reject any high bits that would be
380                // silently dropped by the wire encoding below.
381                if value >> precision_bits != 0 {
382                    return Err(fmt!(
383                        InvalidBind,
384                        "geohash value 0x{:X} has bits set above precision_bits {}",
385                        value,
386                        precision_bits
387                    ));
388                }
389            }
390            varint::encode_u64(*precision_bits as u64, out);
391        }
392        // VARCHAR/BINARY: (non_null + 1) × u32_le offsets array — only
393        // emitted on the non-null branch. Java's QwpEgressRequestDecoder
394        // (TYPE_VARCHAR) reads these 8 bytes only when isNull == false; on
395        // the null branch it advances p by zero, so emitting an empty
396        // offsets array here would be re-read as part of the *next* bind.
397        Bind::Varchar(s) => write_varlen_offsets(&[s.len()], out)?,
398        Bind::Binary(b) => write_varlen_offsets(&[b.len()], out)?,
399        _ => {}
400    }
401
402    if null {
403        return Ok(());
404    }
405
406    // Value bytes (non_null × per-type size).
407    match bind {
408        Bind::Null(_)
409        | Bind::NullVarchar
410        | Bind::NullBinary
411        | Bind::NullDecimal64 { .. }
412        | Bind::NullDecimal128 { .. }
413        | Bind::NullDecimal256 { .. }
414        | Bind::NullGeohash { .. } => unreachable!("handled above"),
415
416        // BOOLEAN is bit-packed: 1 row → 1 byte holding bit 0.
417        Bind::Bool(v) => out.push(if *v { 0x01 } else { 0x00 }),
418        Bind::I8(v) => out.push(*v as u8),
419        Bind::I16(v) => out.extend_from_slice(&v.to_le_bytes()),
420        Bind::I32(v) => out.extend_from_slice(&v.to_le_bytes()),
421        Bind::I64(v) => out.extend_from_slice(&v.to_le_bytes()),
422        Bind::F32(v) => out.extend_from_slice(&v.to_le_bytes()),
423        Bind::F64(v) => out.extend_from_slice(&v.to_le_bytes()),
424        Bind::Char(v) => out.extend_from_slice(&v.to_le_bytes()),
425        Bind::TimestampMicros(v) | Bind::TimestampNanos(v) | Bind::DateMillis(v) => {
426            out.extend_from_slice(&v.to_le_bytes());
427        }
428        Bind::Uuid(b) => out.extend_from_slice(b),
429        Bind::Long256(b) => out.extend_from_slice(b),
430        // `Bind::Ipv4` / `Bind::Binary` are normally rejected client-side
431        // by `check_bindable` — see `PHASE 1 SERVER COMPATIBILITY` at
432        // module top. The encoder arms stay wired for forward
433        // compatibility and to handle bind-sets encoded without going
434        // through `QueryRequestBuilder::build`.
435        Bind::Ipv4(addr) => out.extend_from_slice(&u32::from(*addr).to_le_bytes()),
436        Bind::Decimal64 { value, .. } => out.extend_from_slice(&value.to_le_bytes()),
437        Bind::Decimal128 { value, .. } => out.extend_from_slice(&value.to_le_bytes()),
438        Bind::Decimal256 { bytes, .. } => out.extend_from_slice(bytes),
439        Bind::Geohash {
440            value,
441            precision_bits,
442        } => {
443            let bw = (*precision_bits as usize).div_ceil(8);
444            let bytes = value.to_le_bytes();
445            out.extend_from_slice(&bytes[..bw]);
446        }
447        Bind::Varchar(s) => out.extend_from_slice(s.as_bytes()),
448        Bind::Binary(b) => out.extend_from_slice(b),
449    }
450
451    Ok(())
452}
453
454fn write_varlen_offsets(byte_lens: &[usize], out: &mut Vec<u8>) -> Result<()> {
455    let mut total: u32 = 0;
456    out.extend_from_slice(&total.to_le_bytes());
457    for &len in byte_lens {
458        let len32 = u32::try_from(len)
459            .map_err(|_| fmt!(InvalidBind, "varlen bind value too large: {} bytes", len))?;
460        total = total
461            .checked_add(len32)
462            .ok_or_else(|| fmt!(InvalidBind, "varlen bind offsets overflow u32"))?;
463        out.extend_from_slice(&total.to_le_bytes());
464    }
465    Ok(())
466}
467
468/// Reject bind kinds the Phase 1 server doesn't decode, so the user
469/// sees a typed `InvalidBind` instead of a server `QUERY_ERROR` whose
470/// `request_id=0` breaks correlation.
471///
472/// Set membership (Symbol, Binary, Ipv4, DoubleArray, LongArray) and
473/// the per-kind server-side rationale are documented once in the
474/// `PHASE 1 SERVER COMPATIBILITY` block at the top of this module —
475/// keep that block in sync if this match list changes.
476pub fn check_bindable(kind: ColumnKind) -> Result<()> {
477    match kind {
478        ColumnKind::Symbol
479        | ColumnKind::Binary
480        | ColumnKind::Ipv4
481        | ColumnKind::DoubleArray
482        | ColumnKind::LongArray => Err(fmt!(
483            InvalidBind,
484            "bind not supported for type {} (0x{:02X})",
485            kind.name(),
486            kind.as_u8()
487        )),
488        _ => Ok(()),
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    fn enc(b: Bind) -> Vec<u8> {
497        let mut out = Vec::new();
498        encode_bind(&b, &mut out).unwrap();
499        out
500    }
501
502    // --- Simple null + value paths -----------------------------------------
503
504    #[test]
505    fn simple_null_layout() {
506        // type_code=Long(0x05), null_flag=0x01, bitmap=0x01
507        assert_eq!(
508            enc(Bind::Null(SimpleNullKind::Long)),
509            vec![0x05, 0x01, 0x01]
510        );
511    }
512
513    #[test]
514    fn bool_layout() {
515        assert_eq!(enc(Bind::Bool(true)), vec![0x01, 0x00, 0x01]);
516        assert_eq!(enc(Bind::Bool(false)), vec![0x01, 0x00, 0x00]);
517    }
518
519    #[test]
520    fn i32_le() {
521        assert_eq!(
522            enc(Bind::I32(0x01020304)),
523            vec![0x04, 0x00, 0x04, 0x03, 0x02, 0x01]
524        );
525    }
526
527    #[test]
528    fn i64_le() {
529        assert_eq!(
530            enc(Bind::I64(0x0102_0304_0506_0708)),
531            vec![0x05, 0x00, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
532        );
533    }
534
535    #[test]
536    fn f64_le() {
537        let mut expected = vec![0x07, 0x00];
538        expected.extend_from_slice(&1.0f64.to_le_bytes());
539        assert_eq!(enc(Bind::F64(1.0)), expected);
540    }
541
542    #[test]
543    fn ipv4_le() {
544        let bytes = enc(Bind::Ipv4(Ipv4Addr::new(192, 168, 1, 1)));
545        assert_eq!(bytes, vec![0x18, 0x00, 0x01, 0x01, 0xA8, 0xC0]);
546    }
547
548    #[test]
549    fn uuid_passthrough() {
550        let raw = [
551            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
552            0x0F, 0x10,
553        ];
554        let bytes = enc(Bind::Uuid(raw));
555        assert_eq!(bytes[0], 0x0C);
556        assert_eq!(bytes[1], 0x00);
557        assert_eq!(&bytes[2..], &raw);
558    }
559
560    #[test]
561    fn long256_passthrough() {
562        let raw: [u8; 32] = std::array::from_fn(|i| i as u8);
563        let bytes = enc(Bind::Long256(raw));
564        assert_eq!(bytes[0], 0x0D);
565        assert_eq!(bytes[1], 0x00);
566        assert_eq!(&bytes[2..], &raw);
567    }
568
569    #[test]
570    fn char_layout() {
571        // CHAR (0x16), 'A' = 0x0041 LE
572        assert_eq!(enc(Bind::Char(b'A' as u16)), vec![0x16, 0x00, 0x41, 0x00]);
573    }
574
575    // --- Decimal -----------------------------------------------------------
576
577    #[test]
578    fn decimal64_value_layout() {
579        let bytes = enc(Bind::Decimal64 {
580            value: 12345,
581            scale: 2,
582        });
583        assert_eq!(bytes[0], 0x13);
584        assert_eq!(bytes[1], 0x00);
585        assert_eq!(bytes[2], 0x02);
586        assert_eq!(&bytes[3..], &12345i64.to_le_bytes());
587    }
588
589    #[test]
590    fn decimal64_null_carries_scale() {
591        // type=0x13, null_flag=0x01, bitmap=0x01, scale=4
592        assert_eq!(
593            enc(Bind::NullDecimal64 { scale: 4 }),
594            vec![0x13, 0x01, 0x01, 0x04]
595        );
596    }
597
598    #[test]
599    fn decimal_scale_negative_rejected() {
600        // Encode-time check: scale must be within the per-width bound.
601        // Without this guard, `*scale as u8` would emit 0xFF and the
602        // server would later return a generic QUERY_ERROR.
603        for bind in [
604            Bind::Decimal64 {
605                value: 0,
606                scale: -1,
607            },
608            Bind::Decimal128 {
609                value: 0,
610                scale: -1,
611            },
612            Bind::Decimal256 {
613                bytes: [0; 32],
614                scale: -1,
615            },
616            Bind::NullDecimal64 { scale: -1 },
617            Bind::NullDecimal128 { scale: -1 },
618            Bind::NullDecimal256 { scale: -1 },
619        ] {
620            let mut out = Vec::new();
621            let err = encode_bind(&bind, &mut out).unwrap_err();
622            assert_eq!(err.code(), crate::ErrorCode::InvalidBind);
623            assert!(
624                err.msg().contains("decimal scale"),
625                "expected scale error msg, got: {}",
626                err.msg()
627            );
628        }
629    }
630
631    #[test]
632    fn decimal_scale_above_max_rejected() {
633        // Each width rejects the first value above its own per-width cap
634        // (DECIMAL64 > 18, DECIMAL128 > 38, DECIMAL256 > 76).
635        for bind in [
636            Bind::Decimal64 {
637                value: 0,
638                scale: DECIMAL64_MAX_SCALE + 1,
639            },
640            Bind::NullDecimal128 {
641                scale: DECIMAL128_MAX_SCALE + 1,
642            },
643            Bind::NullDecimal256 {
644                scale: DECIMAL256_MAX_SCALE + 1,
645            },
646        ] {
647            let mut out = Vec::new();
648            let err = encode_bind(&bind, &mut out).unwrap_err();
649            assert_eq!(err.code(), crate::ErrorCode::InvalidBind);
650        }
651    }
652
653    #[test]
654    fn decimal_scale_at_boundaries_accepted() {
655        // 0 and each width's per-width MAX_SCALE must encode cleanly.
656        let cases = [
657            Bind::NullDecimal64 { scale: 0 },
658            Bind::NullDecimal64 {
659                scale: DECIMAL64_MAX_SCALE,
660            },
661            Bind::NullDecimal128 {
662                scale: DECIMAL128_MAX_SCALE,
663            },
664            Bind::NullDecimal256 {
665                scale: DECIMAL256_MAX_SCALE,
666            },
667        ];
668        for bind in cases {
669            let scale = match bind {
670                Bind::NullDecimal64 { scale }
671                | Bind::NullDecimal128 { scale }
672                | Bind::NullDecimal256 { scale } => scale,
673                _ => unreachable!(),
674            };
675            let mut out = Vec::new();
676            encode_bind(&bind, &mut out).unwrap();
677            assert_eq!(out.last().copied(), Some(scale as u8));
678        }
679    }
680
681    #[test]
682    fn decimal128_value_layout() {
683        let bytes = enc(Bind::Decimal128 {
684            value: -42,
685            scale: 6,
686        });
687        assert_eq!(bytes[0], 0x14);
688        assert_eq!(bytes[1], 0x00);
689        assert_eq!(bytes[2], 0x06);
690        assert_eq!(&bytes[3..], &(-42i128).to_le_bytes());
691    }
692
693    #[test]
694    fn decimal128_null_carries_scale() {
695        assert_eq!(
696            enc(Bind::NullDecimal128 { scale: 8 }),
697            vec![0x14, 0x01, 0x01, 0x08]
698        );
699    }
700
701    #[test]
702    fn decimal256_value_layout() {
703        let raw: [u8; 32] = std::array::from_fn(|i| (i + 1) as u8);
704        let bytes = enc(Bind::Decimal256 {
705            bytes: raw,
706            scale: 12,
707        });
708        assert_eq!(bytes[0], 0x15);
709        assert_eq!(bytes[1], 0x00);
710        assert_eq!(bytes[2], 0x0C);
711        assert_eq!(&bytes[3..], &raw);
712    }
713
714    #[test]
715    fn decimal256_null_carries_scale() {
716        assert_eq!(
717            enc(Bind::NullDecimal256 { scale: 18 }),
718            vec![0x15, 0x01, 0x01, 0x12]
719        );
720    }
721
722    // --- Geohash -----------------------------------------------------------
723
724    #[test]
725    fn geohash_value_layout() {
726        // 8 bits → 1 byte; varint(8) = 0x08
727        let bytes = enc(Bind::Geohash {
728            value: 0xAB,
729            precision_bits: 8,
730        });
731        assert_eq!(bytes, vec![0x0E, 0x00, 0x08, 0xAB]);
732    }
733
734    #[test]
735    fn geohash_60_bits_writes_8_bytes() {
736        let bytes = enc(Bind::Geohash {
737            value: 0x0102_0304_0506_0708,
738            precision_bits: 60,
739        });
740        // varint(60) = 0x3C
741        let mut expected = vec![0x0E, 0x00, 0x3C];
742        expected.extend_from_slice(&0x0102_0304_0506_0708u64.to_le_bytes());
743        assert_eq!(bytes, expected);
744    }
745
746    #[test]
747    fn geohash_null_carries_precision() {
748        // varint(20) = 0x14
749        assert_eq!(
750            enc(Bind::NullGeohash { precision_bits: 20 }),
751            vec![0x0E, 0x01, 0x01, 0x14]
752        );
753    }
754
755    #[test]
756    fn geohash_invalid_precision_rejected() {
757        let mut out = Vec::new();
758        let err = encode_bind(
759            &Bind::Geohash {
760                value: 0,
761                precision_bits: 0,
762            },
763            &mut out,
764        )
765        .unwrap_err();
766        assert_eq!(err.code(), crate::ErrorCode::InvalidBind);
767    }
768
769    #[test]
770    fn geohash_value_above_precision_rejected() {
771        let mut out = Vec::new();
772        let err = encode_bind(
773            &Bind::Geohash {
774                value: u64::MAX,
775                precision_bits: 8,
776            },
777            &mut out,
778        )
779        .unwrap_err();
780        assert_eq!(err.code(), crate::ErrorCode::InvalidBind);
781    }
782
783    // --- Varchar / Binary --------------------------------------------------
784
785    #[test]
786    fn varchar_value_layout() {
787        let bytes = enc(Bind::Varchar("hi".into()));
788        // 0x0F, 0x00, offsets [0, 2] (8 bytes), then "hi"
789        let expected = vec![0x0F, 0x00, 0, 0, 0, 0, 2, 0, 0, 0, b'h', b'i'];
790        assert_eq!(bytes, expected);
791    }
792
793    #[test]
794    fn varchar_null_emits_no_offsets_array() {
795        // 0x0F, 0x01, 0x01 — no trailing offsets. Java's TYPE_VARCHAR
796        // bind decoder skips offsets on the null branch; emitting them
797        // would corrupt any following bind in the same QUERY_REQUEST.
798        assert_eq!(enc(Bind::NullVarchar), vec![0x0F, 0x01, 0x01]);
799    }
800
801    #[test]
802    fn binary_value_layout() {
803        let bytes = enc(Bind::Binary(vec![0xDE, 0xAD]));
804        // 0x17, 0x00, [0, 2] offsets, then 0xDE 0xAD
805        let expected = vec![0x17, 0x00, 0, 0, 0, 0, 2, 0, 0, 0, 0xDE, 0xAD];
806        assert_eq!(bytes, expected);
807    }
808
809    #[test]
810    fn binary_null_emits_no_offsets_array() {
811        // Mirrors NullVarchar: no trailing offsets on the null branch.
812        assert_eq!(enc(Bind::NullBinary), vec![0x17, 0x01, 0x01]);
813    }
814
815    #[test]
816    fn null_varchar_then_i32_concatenates_cleanly() {
817        // Regression: previously NullVarchar emitted 4 trailing zero offset
818        // bytes that the server's bind decoder did NOT consume, so the next
819        // bind's leading bytes were misread.
820        let mut out = Vec::new();
821        encode_bind(&Bind::NullVarchar, &mut out).unwrap();
822        encode_bind(&Bind::I32(7), &mut out).unwrap();
823        // [type=Varchar, null_flag, bitmap] || [type=Int, null_flag, 4 LE bytes]
824        assert_eq!(
825            out,
826            vec![0x0F, 0x01, 0x01, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00]
827        );
828    }
829
830    // --- check_bindable ----------------------------------------------------
831
832    #[test]
833    fn check_bindable_rejects_server_unsupported() {
834        // Per the Java client, server doesn't accept these as binds.
835        assert!(check_bindable(ColumnKind::Symbol).is_err());
836        assert!(check_bindable(ColumnKind::Binary).is_err());
837        assert!(check_bindable(ColumnKind::Ipv4).is_err());
838        assert!(check_bindable(ColumnKind::DoubleArray).is_err());
839        assert!(check_bindable(ColumnKind::LongArray).is_err());
840    }
841
842    #[test]
843    fn check_bindable_accepts_remaining_types() {
844        for k in [
845            ColumnKind::Boolean,
846            ColumnKind::Byte,
847            ColumnKind::Short,
848            ColumnKind::Int,
849            ColumnKind::Long,
850            ColumnKind::Float,
851            ColumnKind::Double,
852            ColumnKind::Timestamp,
853            ColumnKind::TimestampNanos,
854            ColumnKind::Date,
855            ColumnKind::Uuid,
856            ColumnKind::Long256,
857            ColumnKind::Char,
858            ColumnKind::Varchar,
859            ColumnKind::Decimal64,
860            ColumnKind::Decimal128,
861            ColumnKind::Decimal256,
862            ColumnKind::Geohash,
863        ] {
864            check_bindable(k).unwrap_or_else(|_| panic!("{}", k.name()));
865        }
866    }
867
868    #[test]
869    fn simple_null_kind_try_from_rejects_kinds_with_column_args() {
870        // Each of these kinds requires column-level metadata in its null wire
871        // body (DECIMAL\* scale, GEOHASH precision_bits) or a different null
872        // layout (VARCHAR / BINARY skip the offsets array on null) — they
873        // route through dedicated `Null*` variants and must NOT be
874        // representable as `Bind::Null(SimpleNullKind)`. Same for SYMBOL /
875        // DOUBLE_ARRAY / LONG_ARRAY which the server rejects entirely as
876        // bind values.
877        for kind in [
878            ColumnKind::Varchar,
879            ColumnKind::Binary,
880            ColumnKind::Decimal64,
881            ColumnKind::Decimal128,
882            ColumnKind::Decimal256,
883            ColumnKind::Geohash,
884            ColumnKind::Symbol,
885            ColumnKind::DoubleArray,
886            ColumnKind::LongArray,
887        ] {
888            let r = SimpleNullKind::try_from(kind);
889            assert!(
890                r.is_err(),
891                "{} must not convert to SimpleNullKind",
892                kind.name()
893            );
894        }
895    }
896
897    #[test]
898    fn null_bind_accepts_simple_kinds() {
899        for kind in [
900            SimpleNullKind::Boolean,
901            SimpleNullKind::Byte,
902            SimpleNullKind::Short,
903            SimpleNullKind::Int,
904            SimpleNullKind::Long,
905            SimpleNullKind::Float,
906            SimpleNullKind::Double,
907            SimpleNullKind::Timestamp,
908            SimpleNullKind::TimestampNanos,
909            SimpleNullKind::Date,
910            SimpleNullKind::Uuid,
911            SimpleNullKind::Long256,
912            SimpleNullKind::Char,
913            SimpleNullKind::Ipv4,
914        ] {
915            let mut out = Vec::new();
916            encode_bind(&Bind::Null(kind), &mut out).unwrap_or_else(|_| {
917                panic!("Bind::Null({}) should encode", kind.as_column_kind().name())
918            });
919            // Simple null layout: [type, null_flag=0x01, bitmap=0x01]
920            assert_eq!(out, vec![kind.as_column_kind().as_u8(), 0x01, 0x01]);
921        }
922    }
923
924    #[test]
925    fn null_bind_kind_preserved() {
926        assert_eq!(
927            Bind::NullDecimal64 { scale: 0 }.kind(),
928            ColumnKind::Decimal64
929        );
930        assert_eq!(Bind::NullVarchar.kind(), ColumnKind::Varchar);
931        assert_eq!(
932            Bind::NullGeohash { precision_bits: 8 }.kind(),
933            ColumnKind::Geohash
934        );
935    }
936
937    // -----------------------------------------------------------------------
938    // Property-based fuzz: random value → encode → manually parse the wire
939    // bytes → assert the round-trip matches the input bit-for-bit. Ports
940    // `core/.../QwpEgressBindFuzzTest.java` from the OSS questdb repo. The
941    // Java original drives a live `TestServerMain` so the server does the
942    // decode; here we re-implement the per-type wire reader inline because
943    // the Rust crate ships only the encoder (the server is the canonical
944    // decoder in production). The reader mirrors the layout documented at
945    // the top of this file so any encoder change that drifts from the spec
946    // surfaces here as a fuzz failure.
947    // -----------------------------------------------------------------------
948    mod fuzz {
949        use super::*;
950        use proptest::prelude::*;
951
952        /// Strip the `[type_code, null_flag=0x00]` prefix from a non-null
953        /// value bind, returning the remaining payload bytes. Panics —
954        /// which proptest reports as a shrinkable failure — if the prefix
955        /// doesn't match.
956        fn body_of_non_null(expected_kind: ColumnKind, encoded: &[u8]) -> &[u8] {
957            assert!(encoded.len() >= 2, "encoded bind too short");
958            assert_eq!(
959                encoded[0],
960                expected_kind.as_u8(),
961                "type code mismatch: encoded={:02x} expected={:02x} ({})",
962                encoded[0],
963                expected_kind.as_u8(),
964                expected_kind.name()
965            );
966            assert_eq!(encoded[1], 0x00, "null_flag must be 0x00 for non-null bind");
967            &encoded[2..]
968        }
969
970        // ---- Scalar round-trips (Java's testFuzzIntegralBindsProjection
971        // territory: long, int, short, byte, bool) ----------------------
972
973        proptest! {
974            #![proptest_config(ProptestConfig {
975                cases: 200,
976                .. ProptestConfig::default()
977            })]
978
979            #[test]
980            fn fuzz_bool(v: bool) {
981                let bytes = enc(Bind::Bool(v));
982                let body = body_of_non_null(ColumnKind::Boolean, &bytes);
983                prop_assert_eq!(body, &[v as u8][..]);
984            }
985
986            #[test]
987            fn fuzz_i8(v: i8) {
988                let bytes = enc(Bind::I8(v));
989                let body = body_of_non_null(ColumnKind::Byte, &bytes);
990                prop_assert_eq!(body, &[v as u8][..]);
991            }
992
993            #[test]
994            fn fuzz_i16(v: i16) {
995                let bytes = enc(Bind::I16(v));
996                let body = body_of_non_null(ColumnKind::Short, &bytes);
997                prop_assert_eq!(body.len(), 2);
998                let got = i16::from_le_bytes(body.try_into().unwrap());
999                prop_assert_eq!(got, v);
1000            }
1001
1002            #[test]
1003            fn fuzz_i32(v: i32) {
1004                let bytes = enc(Bind::I32(v));
1005                let body = body_of_non_null(ColumnKind::Int, &bytes);
1006                prop_assert_eq!(body.len(), 4);
1007                let got = i32::from_le_bytes(body.try_into().unwrap());
1008                prop_assert_eq!(got, v);
1009            }
1010
1011            #[test]
1012            fn fuzz_i64(v: i64) {
1013                let bytes = enc(Bind::I64(v));
1014                let body = body_of_non_null(ColumnKind::Long, &bytes);
1015                prop_assert_eq!(body.len(), 8);
1016                let got = i64::from_le_bytes(body.try_into().unwrap());
1017                prop_assert_eq!(got, v);
1018            }
1019
1020            // -- Floats: compare by raw bits so NaN round-trips. The Java
1021            // reference test uses `Double.isNaN(d)` plus `==` for finite
1022            // values; raw-bits is equivalent and also catches -0.0 vs 0.0
1023            // (the encoder must not normalise — that's the server's job).
1024
1025            #[test]
1026            fn fuzz_f32_bits(bits: u32) {
1027                let v = f32::from_bits(bits);
1028                let bytes = enc(Bind::F32(v));
1029                let body = body_of_non_null(ColumnKind::Float, &bytes);
1030                prop_assert_eq!(body.len(), 4);
1031                let got = f32::from_le_bytes(body.try_into().unwrap());
1032                prop_assert_eq!(got.to_bits(), v.to_bits());
1033            }
1034
1035            #[test]
1036            fn fuzz_f64_bits(bits: u64) {
1037                let v = f64::from_bits(bits);
1038                let bytes = enc(Bind::F64(v));
1039                let body = body_of_non_null(ColumnKind::Double, &bytes);
1040                prop_assert_eq!(body.len(), 8);
1041                let got = f64::from_le_bytes(body.try_into().unwrap());
1042                prop_assert_eq!(got.to_bits(), v.to_bits());
1043            }
1044
1045            // -- Temporal scalars: same wire as I64 but typed differently.
1046
1047            #[test]
1048            fn fuzz_timestamp_micros(v: i64) {
1049                let bytes = enc(Bind::TimestampMicros(v));
1050                let body = body_of_non_null(ColumnKind::Timestamp, &bytes);
1051                prop_assert_eq!(i64::from_le_bytes(body.try_into().unwrap()), v);
1052            }
1053
1054            #[test]
1055            fn fuzz_timestamp_nanos(v: i64) {
1056                let bytes = enc(Bind::TimestampNanos(v));
1057                let body = body_of_non_null(ColumnKind::TimestampNanos, &bytes);
1058                prop_assert_eq!(i64::from_le_bytes(body.try_into().unwrap()), v);
1059            }
1060
1061            #[test]
1062            fn fuzz_date_millis(v: i64) {
1063                let bytes = enc(Bind::DateMillis(v));
1064                let body = body_of_non_null(ColumnKind::Date, &bytes);
1065                prop_assert_eq!(i64::from_le_bytes(body.try_into().unwrap()), v);
1066            }
1067
1068            // -- 16-bit Char: u16 LE.
1069
1070            #[test]
1071            fn fuzz_char(v: u16) {
1072                let bytes = enc(Bind::Char(v));
1073                let body = body_of_non_null(ColumnKind::Char, &bytes);
1074                prop_assert_eq!(body.len(), 2);
1075                let got = u16::from_le_bytes(body.try_into().unwrap());
1076                prop_assert_eq!(got, v);
1077            }
1078
1079            // -- IPv4: 4 bytes LE.
1080
1081            #[test]
1082            fn fuzz_ipv4(octets: [u8; 4]) {
1083                let addr = Ipv4Addr::from(u32::from_be_bytes(octets));
1084                let bytes = enc(Bind::Ipv4(addr));
1085                let body = body_of_non_null(ColumnKind::Ipv4, &bytes);
1086                prop_assert_eq!(body.len(), 4);
1087                let got = Ipv4Addr::from(u32::from_le_bytes(body.try_into().unwrap()));
1088                prop_assert_eq!(got, addr);
1089            }
1090
1091            // -- Wide raw blobs: 16-byte UUID + 32-byte LONG256.
1092
1093            #[test]
1094            fn fuzz_uuid(raw in proptest::array::uniform16(any::<u8>())) {
1095                let bytes = enc(Bind::Uuid(raw));
1096                let body = body_of_non_null(ColumnKind::Uuid, &bytes);
1097                prop_assert_eq!(body, &raw[..]);
1098            }
1099
1100            #[test]
1101            fn fuzz_long256(raw in proptest::array::uniform32(any::<u8>())) {
1102                let bytes = enc(Bind::Long256(raw));
1103                let body = body_of_non_null(ColumnKind::Long256, &bytes);
1104                prop_assert_eq!(body, &raw[..]);
1105            }
1106
1107            // -- DECIMAL64 / DECIMAL128 / DECIMAL256: scale (i8, per-width
1108            // 0..=18 / 0..=38 / 0..=76) + LE mantissa bytes. Scale comes
1109            // first on the wire per the docs at the top of this file.
1110
1111            #[test]
1112            fn fuzz_decimal64(value: i64, scale in 0i8..=DECIMAL64_MAX_SCALE) {
1113                let bytes = enc(Bind::Decimal64 { value, scale });
1114                let body = body_of_non_null(ColumnKind::Decimal64, &bytes);
1115                prop_assert_eq!(body.len(), 1 + 8);
1116                prop_assert_eq!(body[0] as i8, scale);
1117                prop_assert_eq!(i64::from_le_bytes(body[1..].try_into().unwrap()), value);
1118            }
1119
1120            #[test]
1121            fn fuzz_decimal128(value: i128, scale in 0i8..=DECIMAL128_MAX_SCALE) {
1122                let bytes = enc(Bind::Decimal128 { value, scale });
1123                let body = body_of_non_null(ColumnKind::Decimal128, &bytes);
1124                prop_assert_eq!(body.len(), 1 + 16);
1125                prop_assert_eq!(body[0] as i8, scale);
1126                prop_assert_eq!(i128::from_le_bytes(body[1..].try_into().unwrap()), value);
1127            }
1128
1129            #[test]
1130            fn fuzz_decimal256(
1131                raw in proptest::array::uniform32(any::<u8>()),
1132                scale in 0i8..=DECIMAL256_MAX_SCALE,
1133            ) {
1134                let bytes = enc(Bind::Decimal256 { bytes: raw, scale });
1135                let body = body_of_non_null(ColumnKind::Decimal256, &bytes);
1136                prop_assert_eq!(body.len(), 1 + 32);
1137                prop_assert_eq!(body[0] as i8, scale);
1138                prop_assert_eq!(&body[1..], &raw[..]);
1139            }
1140
1141            // -- GEOHASH: varint precision (1..=60) + ceil(precision/8) bytes.
1142
1143            #[test]
1144            fn fuzz_geohash(raw_value: u64, precision_bits in 1u8..=60) {
1145                // The encoder rejects values with bits set above
1146                // `precision_bits` (see check_bindable + encode_geohash). The
1147                // Java reference test routes geohash binds through SQL, where
1148                // the server normalises; here we mask upfront so the fuzz
1149                // exercises the encoder's value-shaping rather than its
1150                // out-of-range rejection (already covered by the unit tests
1151                // above).
1152                let mask = if precision_bits == 64 {
1153                    !0u64
1154                } else {
1155                    (1u64 << precision_bits) - 1
1156                };
1157                let value = raw_value & mask;
1158                let bytes = enc(Bind::Geohash { value, precision_bits });
1159                let body = body_of_non_null(ColumnKind::Geohash, &bytes);
1160                // Precision is a varint; for the 1..=60 range it always fits
1161                // in a single byte (high bit clear), so the layout is
1162                // `precision_byte || ceil(precision_bits/8) value bytes`.
1163                prop_assert_eq!(body[0], precision_bits);
1164                let byte_width = (precision_bits as usize).div_ceil(8);
1165                prop_assert_eq!(body.len(), 1 + byte_width);
1166                let mut buf = [0u8; 8];
1167                buf[..byte_width].copy_from_slice(&body[1..]);
1168                let got = u64::from_le_bytes(buf);
1169                prop_assert_eq!(got, value);
1170            }
1171
1172            // -- VARCHAR / BINARY: offsets array (2 × u32_le for one row:
1173            // `[0, byte_len]`) + concatenated bytes. UTF-8 validity for
1174            // VARCHAR is the spec's responsibility; we run it through with
1175            // arbitrary `String`s — proptest's default `String` strategy
1176            // covers a mix of ASCII and multibyte codepoints.
1177
1178            #[test]
1179            fn fuzz_varchar(s in ".{0,32}") {
1180                let bytes = enc(Bind::Varchar(s.clone()));
1181                let body = body_of_non_null(ColumnKind::Varchar, &bytes);
1182                let utf8_bytes = s.as_bytes();
1183                prop_assert_eq!(body.len(), 8 + utf8_bytes.len());
1184                let offset0 = u32::from_le_bytes(body[0..4].try_into().unwrap());
1185                let offset1 = u32::from_le_bytes(body[4..8].try_into().unwrap());
1186                prop_assert_eq!(offset0, 0);
1187                prop_assert_eq!(offset1 as usize, utf8_bytes.len());
1188                prop_assert_eq!(&body[8..], utf8_bytes);
1189            }
1190
1191            #[test]
1192            fn fuzz_binary(buf in proptest::collection::vec(any::<u8>(), 0..32)) {
1193                let bytes = enc(Bind::Binary(buf.clone()));
1194                let body = body_of_non_null(ColumnKind::Binary, &bytes);
1195                prop_assert_eq!(body.len(), 8 + buf.len());
1196                let offset0 = u32::from_le_bytes(body[0..4].try_into().unwrap());
1197                let offset1 = u32::from_le_bytes(body[4..8].try_into().unwrap());
1198                prop_assert_eq!(offset0, 0);
1199                prop_assert_eq!(offset1 as usize, buf.len());
1200                prop_assert_eq!(&body[8..], &buf[..]);
1201            }
1202        }
1203    }
1204}