hyperdb_api/params.rs
1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Parameter encoding for parameterized queries.
5//!
6//! This module provides the [`ToSqlParam`] trait for type-safe parameter encoding
7//! in parameterized SQL queries, preventing SQL injection attacks.
8//!
9//! # SQL Injection Prevention
10//!
11//! Using parameterized queries is the safest way to include user input in SQL:
12//!
13//! ```no_run
14//! # use hyperdb_api::{Connection, Result};
15//! # fn example(conn: &Connection, user_input: &str) -> Result<()> {
16//! // DANGEROUS - vulnerable to SQL injection:
17//! let query = format!("SELECT * FROM users WHERE name = '{}'", user_input);
18//!
19//! // SAFE - parameterized query:
20//! let result = conn.query_params("SELECT * FROM users WHERE name = $1", &[&user_input])?;
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! # Supported Types
26//!
27//! The following types implement [`ToSqlParam`]:
28//!
29//! - Integers: `i16`, `i32`, `i64`
30//! - Floats: `f32`, `f64`
31//! - `bool`
32//! - `&str`, `String`
33//! - Bytes: `&[u8]`, `Vec<u8>`
34//! - Date/time types: `Date`, `Time`, `Timestamp`, `OffsetTimestamp`
35//! - `Interval`
36//! - `Numeric` — **whole numbers only (`scale == 0`)**; Hyper rejects
37//! scaled binary NUMERIC params (see the `Numeric` impl and issue #132)
38//! - `serde_json::Value` (binds as PostgreSQL `json`)
39//! - `Option<T>` where `T: ToSqlParam` (for nullable parameters)
40//! - `&T` where `T: ToSqlParam`
41//!
42//! Note: `Geography` does **not** implement `ToSqlParam` — Hyper has no
43//! PostgreSQL-binary input function for the geography type (issue #133).
44//! Use the [`Inserter`](crate::Inserter) (`IntoValue`) path to write
45//! geography values instead.
46//!
47//! # Example
48//!
49//! ```no_run
50//! use hyperdb_api::{Connection, CreateMode, ToSqlParam, Result};
51//!
52//! fn find_user(conn: &Connection, user_id: i32, name: &str) -> Result<()> {
53//! // Multiple parameters with different types
54//! let result = conn.query_params(
55//! "SELECT * FROM users WHERE id = $1 AND name = $2",
56//! &[&user_id, &name],
57//! )?;
58//! Ok(())
59//! }
60//! ```
61//!
62//! # Mapping parameterized results into structs
63//!
64//! [`query_params`](crate::Connection::query_params) returns raw
65//! [`Row`](crate::Row)s. To map a parameterized query's results straight into
66//! a [`FromRow`](crate::FromRow) struct in one call, use the `_as_params`
67//! variants — [`fetch_one_as_params`](crate::Connection::fetch_one_as_params),
68//! [`fetch_all_as_params`](crate::Connection::fetch_all_as_params), and
69//! [`stream_as_params`](crate::Connection::stream_as_params) (and their
70//! [`AsyncConnection`](crate::AsyncConnection) equivalents).
71
72use hyperdb_api_core::types::{
73 oids, Date, Interval, Numeric, OffsetTimestamp, Oid, Time, Timestamp,
74};
75
76/// Trait for types that can be used as parameters in parameterized SQL queries.
77///
78/// This trait enables type-safe parameter encoding for use with
79/// [`Connection::query_params`](crate::Connection::query_params) and
80/// [`Connection::command_params`](crate::Connection::command_params), and with
81/// the struct-mapping variants
82/// [`fetch_one_as_params`](crate::Connection::fetch_one_as_params),
83/// [`fetch_all_as_params`](crate::Connection::fetch_all_as_params), and
84/// [`stream_as_params`](crate::Connection::stream_as_params).
85///
86/// # Implementing for Custom Types
87///
88/// You can implement this trait for custom types:
89///
90/// ```no_run
91/// # use hyperdb_api::ToSqlParam;
92/// # struct MyType;
93/// # impl MyType { fn to_bytes(&self) -> Vec<u8> { vec![] } }
94/// # impl ToString for MyType { fn to_string(&self) -> String { String::new() } }
95/// impl ToSqlParam for MyType {
96/// fn encode_param(&self) -> Option<Vec<u8>> {
97/// Some(self.to_bytes())
98/// }
99///
100/// fn to_sql_literal(&self) -> String {
101/// format!("'{}'", self.to_string().replace('\'', "''"))
102/// }
103/// }
104/// ```
105pub trait ToSqlParam: Send + Sync {
106 /// Encodes this value as binary bytes for use in parameterized queries.
107 ///
108 /// Returns `None` to represent a SQL NULL value.
109 /// Returns `Some(bytes)` with the binary-encoded value otherwise.
110 fn encode_param(&self) -> Option<Vec<u8>>;
111
112 /// Returns the SQL type OID this parameter should bind as.
113 ///
114 /// The default returns `Oid(0)` (unspecified) which asks the server
115 /// to infer the type from surrounding SQL context. That works for
116 /// clauses like `WHERE column = $1` where the column type is known,
117 /// but not for `INSERT INTO t VALUES ($1, $2)` — those require the
118 /// caller (or the trait impl) to return a concrete OID.
119 ///
120 /// All built-in `ToSqlParam` impls override this with a concrete
121 /// value from [`hyperdb_api_core::types::oids`].
122 fn sql_oid(&self) -> Oid {
123 Oid::new(0)
124 }
125
126 /// Returns the SQL literal representation of this value.
127 ///
128 /// Retained for building DDL statement strings that cannot use
129 /// parameterized queries (e.g. `escape_sql_path` in catalog code).
130 /// The parameterized-query path in
131 /// [`Connection::query_params`](crate::Connection::query_params)
132 /// no longer uses this method — parameters travel as binary bytes
133 /// via `encode_param`.
134 fn to_sql_literal(&self) -> String;
135}
136
137// =============================================================================
138// Integer implementations
139// =============================================================================
140
141impl ToSqlParam for i16 {
142 fn encode_param(&self) -> Option<Vec<u8>> {
143 // PostgreSQL wire-protocol Bind uses big-endian for numeric
144 // binary parameters. (Results come back as little-endian
145 // HyperBinary because we request format code 2 for results;
146 // params use format code 1 = standard PG binary = BE.)
147 Some(self.to_be_bytes().to_vec())
148 }
149
150 fn sql_oid(&self) -> Oid {
151 oids::SMALL_INT
152 }
153
154 fn to_sql_literal(&self) -> String {
155 self.to_string()
156 }
157}
158
159impl ToSqlParam for i32 {
160 fn encode_param(&self) -> Option<Vec<u8>> {
161 Some(self.to_be_bytes().to_vec())
162 }
163
164 fn sql_oid(&self) -> Oid {
165 oids::INT
166 }
167
168 fn to_sql_literal(&self) -> String {
169 self.to_string()
170 }
171}
172
173impl ToSqlParam for i64 {
174 fn encode_param(&self) -> Option<Vec<u8>> {
175 Some(self.to_be_bytes().to_vec())
176 }
177
178 fn sql_oid(&self) -> Oid {
179 oids::BIG_INT
180 }
181
182 fn to_sql_literal(&self) -> String {
183 self.to_string()
184 }
185}
186
187// =============================================================================
188// Float implementations
189// =============================================================================
190
191impl ToSqlParam for f32 {
192 fn encode_param(&self) -> Option<Vec<u8>> {
193 Some(self.to_be_bytes().to_vec())
194 }
195
196 fn sql_oid(&self) -> Oid {
197 oids::FLOAT
198 }
199
200 fn to_sql_literal(&self) -> String {
201 // Handle special float values
202 if self.is_nan() {
203 "'NaN'".to_string()
204 } else if self.is_infinite() {
205 if *self > 0.0 {
206 "'Infinity'".to_string()
207 } else {
208 "'-Infinity'".to_string()
209 }
210 } else {
211 self.to_string()
212 }
213 }
214}
215
216impl ToSqlParam for f64 {
217 fn encode_param(&self) -> Option<Vec<u8>> {
218 Some(self.to_be_bytes().to_vec())
219 }
220
221 fn sql_oid(&self) -> Oid {
222 oids::DOUBLE
223 }
224
225 fn to_sql_literal(&self) -> String {
226 // Handle special float values
227 if self.is_nan() {
228 "'NaN'".to_string()
229 } else if self.is_infinite() {
230 if *self > 0.0 {
231 "'Infinity'".to_string()
232 } else {
233 "'-Infinity'".to_string()
234 }
235 } else {
236 self.to_string()
237 }
238 }
239}
240
241// =============================================================================
242// Boolean implementation
243// =============================================================================
244
245impl ToSqlParam for bool {
246 fn encode_param(&self) -> Option<Vec<u8>> {
247 Some(vec![u8::from(*self)])
248 }
249
250 fn sql_oid(&self) -> Oid {
251 oids::BOOL
252 }
253
254 fn to_sql_literal(&self) -> String {
255 if *self { "TRUE" } else { "FALSE" }.to_string()
256 }
257}
258
259// =============================================================================
260// String implementations
261// =============================================================================
262
263impl ToSqlParam for str {
264 fn encode_param(&self) -> Option<Vec<u8>> {
265 Some(self.as_bytes().to_vec())
266 }
267
268 fn sql_oid(&self) -> Oid {
269 oids::TEXT
270 }
271
272 fn to_sql_literal(&self) -> String {
273 // Escape single quotes by doubling them
274 format!("'{}'", self.replace('\'', "''"))
275 }
276}
277
278impl ToSqlParam for String {
279 fn encode_param(&self) -> Option<Vec<u8>> {
280 Some(self.as_bytes().to_vec())
281 }
282
283 fn sql_oid(&self) -> Oid {
284 oids::TEXT
285 }
286
287 fn to_sql_literal(&self) -> String {
288 format!("'{}'", self.replace('\'', "''"))
289 }
290}
291
292impl ToSqlParam for &str {
293 fn encode_param(&self) -> Option<Vec<u8>> {
294 Some(self.as_bytes().to_vec())
295 }
296
297 fn sql_oid(&self) -> Oid {
298 oids::TEXT
299 }
300
301 fn to_sql_literal(&self) -> String {
302 format!("'{}'", self.replace('\'', "''"))
303 }
304}
305
306// =============================================================================
307// Reference implementations
308// =============================================================================
309
310impl<T: ToSqlParam> ToSqlParam for &T {
311 fn encode_param(&self) -> Option<Vec<u8>> {
312 (*self).encode_param()
313 }
314
315 fn sql_oid(&self) -> Oid {
316 (*self).sql_oid()
317 }
318
319 fn to_sql_literal(&self) -> String {
320 (*self).to_sql_literal()
321 }
322}
323
324// =============================================================================
325// Option implementation (for nullable parameters)
326// =============================================================================
327
328impl<T: ToSqlParam> ToSqlParam for Option<T> {
329 fn encode_param(&self) -> Option<Vec<u8>> {
330 match self {
331 Some(value) => value.encode_param(),
332 None => None, // SQL NULL
333 }
334 }
335
336 fn sql_oid(&self) -> Oid {
337 match self {
338 Some(value) => value.sql_oid(),
339 // For NULL we leave the OID unspecified — server infers
340 // from context, which is the correct behavior for `WHERE
341 // col = $1` with a NULL binding.
342 None => Oid::new(0),
343 }
344 }
345
346 fn to_sql_literal(&self) -> String {
347 match self {
348 Some(value) => value.to_sql_literal(),
349 None => "NULL".to_string(),
350 }
351 }
352}
353
354// =============================================================================
355// Date/Time implementations
356// =============================================================================
357
358impl ToSqlParam for Date {
359 fn encode_param(&self) -> Option<Vec<u8>> {
360 // Date is stored as i32 Julian day offset from 2000-01-01.
361 // Big-endian per the PG Bind protocol (format code 1).
362 Some(self.to_julian_day().to_be_bytes().to_vec())
363 }
364
365 fn sql_oid(&self) -> Oid {
366 oids::DATE
367 }
368
369 fn to_sql_literal(&self) -> String {
370 format!("DATE '{self}'")
371 }
372}
373
374impl ToSqlParam for Time {
375 fn encode_param(&self) -> Option<Vec<u8>> {
376 // Time is stored as i64 microseconds since midnight.
377 Some(self.to_microseconds().to_be_bytes().to_vec())
378 }
379
380 fn sql_oid(&self) -> Oid {
381 oids::TIME
382 }
383
384 fn to_sql_literal(&self) -> String {
385 format!("TIME '{self}'")
386 }
387}
388
389impl ToSqlParam for Timestamp {
390 fn encode_param(&self) -> Option<Vec<u8>> {
391 // Timestamp is stored as i64 microseconds since 2000-01-01.
392 Some(self.to_microseconds().to_be_bytes().to_vec())
393 }
394
395 fn sql_oid(&self) -> Oid {
396 oids::TIMESTAMP
397 }
398
399 fn to_sql_literal(&self) -> String {
400 format!("TIMESTAMP '{self}'")
401 }
402}
403
404impl ToSqlParam for OffsetTimestamp {
405 fn encode_param(&self) -> Option<Vec<u8>> {
406 // OffsetTimestamp is stored as i64 microseconds UTC since 2000-01-01.
407 Some(self.to_microseconds_utc().to_be_bytes().to_vec())
408 }
409
410 fn sql_oid(&self) -> Oid {
411 oids::TIMESTAMP_TZ
412 }
413
414 fn to_sql_literal(&self) -> String {
415 format!("TIMESTAMPTZ '{self}'")
416 }
417}
418
419// =============================================================================
420// Bytes implementation
421// =============================================================================
422
423impl ToSqlParam for [u8] {
424 fn encode_param(&self) -> Option<Vec<u8>> {
425 Some(self.to_vec())
426 }
427
428 fn sql_oid(&self) -> Oid {
429 oids::BYTE_A
430 }
431
432 #[expect(
433 clippy::format_collect,
434 reason = "readable hex/string formatting loop; refactoring to fold! obscures intent"
435 )]
436 fn to_sql_literal(&self) -> String {
437 // Encode as hex bytea literal
438 let hex_str: String = self.iter().map(|b| format!("{b:02x}")).collect();
439 format!("E'\\\\x{hex_str}'")
440 }
441}
442
443impl ToSqlParam for Vec<u8> {
444 fn encode_param(&self) -> Option<Vec<u8>> {
445 Some(self.clone())
446 }
447
448 fn sql_oid(&self) -> Oid {
449 oids::BYTE_A
450 }
451
452 #[expect(
453 clippy::format_collect,
454 reason = "readable hex/string formatting loop; refactoring to fold! obscures intent"
455 )]
456 fn to_sql_literal(&self) -> String {
457 let hex_str: String = self.iter().map(|b| format!("{b:02x}")).collect();
458 format!("E'\\\\x{hex_str}'")
459 }
460}
461
462// =============================================================================
463// Numeric implementation
464// =============================================================================
465
466/// Encode a whole-number (`scale == 0`) `Numeric` as PostgreSQL binary NUMERIC.
467///
468/// Header (i16 BE): `ndigits`, `weight`, `sign` (0x0000 pos / 0x4000 neg),
469/// `dscale = 0`; then `ndigits` base-10000 groups (i16 BE, most-significant
470/// first). The `weight` of the most-significant group is `ndigits - 1` (it
471/// sits at base-10000 position `ndigits-1`), and `dscale` is 0 because there
472/// are no fractional digits.
473///
474/// This handles ONLY `scale == 0`. Correctly encoding a scaled NUMERIC
475/// requires decomposing the *decimal* representation into base-10000 groups
476/// aligned on the decimal point (not decomposing the unscaled integer) — that
477/// is out of scope here because Hyper rejects scaled binary NUMERIC params
478/// regardless (see [`ToSqlParam for Numeric`] and #132). The caller is
479/// responsible for only invoking this with `scale == 0`.
480#[expect(
481 clippy::cast_possible_truncation,
482 clippy::cast_possible_wrap,
483 reason = "an i128 spans at most ~39 decimal digits → ≤10 base-10000 groups; \
484 ndigits and weight always fit in i16"
485)]
486fn pg_numeric_encode_unscaled(unscaled: i128) -> Vec<u8> {
487 let sign_neg = unscaled < 0;
488 let mut mag = unscaled.unsigned_abs();
489
490 // Decompose the integer magnitude into base-10000 groups, least-significant
491 // first, then reverse to most-significant first.
492 let mut groups: Vec<i16> = Vec::new();
493 while mag > 0 {
494 groups.push((mag % 10000) as i16);
495 mag /= 10000;
496 }
497 groups.reverse(); // empty when unscaled == 0
498
499 let ndigits = groups.len() as i16;
500 let weight = if groups.is_empty() { 0 } else { ndigits - 1 };
501
502 let mut buf = Vec::with_capacity(8 + groups.len() * 2);
503 buf.extend_from_slice(&ndigits.to_be_bytes());
504 buf.extend_from_slice(&weight.to_be_bytes());
505 buf.extend_from_slice(&(if sign_neg { 0x4000_i16 } else { 0 }).to_be_bytes());
506 buf.extend_from_slice(&0_i16.to_be_bytes()); // dscale = 0 (whole number)
507 for g in groups {
508 buf.extend_from_slice(&g.to_be_bytes());
509 }
510 buf
511}
512
513impl ToSqlParam for Numeric {
514 /// Binds as PostgreSQL binary NUMERIC. **Only `scale() == 0` (whole
515 /// numbers) is supported.**
516 ///
517 /// Hyper rejects scaled binary NUMERIC params at query time with SQLSTATE
518 /// `0A000` ("cannot handle truncation when reading numerics") — verified
519 /// empirically, and regardless of an explicit `CAST`. So a faithful scaled
520 /// encoder would never succeed anyway; full scaled support is tracked in
521 /// #132.
522 ///
523 /// For `scale() > 0` this returns a header whose `dscale` is set to the
524 /// true scale. The byte payload is therefore NOT a correct PostgreSQL
525 /// NUMERIC for the value (correct scaled encoding requires decimal-aligned
526 /// base-10000 grouping, deferred to #132) — but because `dscale > 0`, Hyper
527 /// rejects it server-side before it can be misinterpreted. The net effect
528 /// is fail-fast: a scaled param errors clearly instead of silently binding
529 /// a wrong whole number.
530 fn encode_param(&self) -> Option<Vec<u8>> {
531 if self.scale() == 0 {
532 return Some(pg_numeric_encode_unscaled(self.unscaled_value()));
533 }
534 // scale > 0: emit the unscaled digits but with dscale = scale so the
535 // server rejects it (0A000) rather than reading a mis-scaled integer.
536 // These bytes are intentionally server-rejected, not a valid value;
537 // see the doc comment and #132.
538 let mut buf = pg_numeric_encode_unscaled(self.unscaled_value());
539 // Overwrite the dscale field (bytes 6..8) with the true scale.
540 let dscale = i16::from(self.scale()).to_be_bytes();
541 buf[6] = dscale[0];
542 buf[7] = dscale[1];
543 Some(buf)
544 }
545 fn sql_oid(&self) -> Oid {
546 oids::NUMERIC
547 }
548 fn to_sql_literal(&self) -> String {
549 self.to_string()
550 } // Display = decimal string
551}
552
553// =============================================================================
554// Interval implementation
555// =============================================================================
556
557impl ToSqlParam for Interval {
558 fn encode_param(&self) -> Option<Vec<u8>> {
559 // PG interval binary (Bind format code 1): i64 microseconds, i32 days,
560 // i32 months — all BIG-endian. NB this differs from Hyper's HyperBinary
561 // `Interval::encode()` which is the same field order but LITTLE-endian.
562 let mut buf = Vec::with_capacity(16);
563 buf.extend_from_slice(&self.microseconds().to_be_bytes());
564 buf.extend_from_slice(&self.days().to_be_bytes());
565 buf.extend_from_slice(&self.months().to_be_bytes());
566 Some(buf)
567 }
568 fn sql_oid(&self) -> Oid {
569 oids::INTERVAL
570 }
571 fn to_sql_literal(&self) -> String {
572 format!("INTERVAL '{self}'")
573 }
574}
575
576// =============================================================================
577// JSON implementation
578// =============================================================================
579
580impl ToSqlParam for serde_json::Value {
581 fn encode_param(&self) -> Option<Vec<u8>> {
582 // PG `json` binary form == the UTF-8 text. (jsonb has a leading
583 // version byte; `json` does not, and oids::JSON is `json`.)
584 // Value::to_string() is compact (no whitespace, no trailing newline)
585 // and correctly escapes embedded quotes — exactly the wire form needed.
586 Some(self.to_string().into_bytes())
587 }
588 fn sql_oid(&self) -> Oid {
589 oids::JSON
590 }
591 fn to_sql_literal(&self) -> String {
592 format!("'{}'", self.to_string().replace('\'', "''"))
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599
600 #[test]
601 fn test_i32_encoding() {
602 // Big-endian per PG Bind format code 1.
603 assert_eq!(42i32.encode_param(), Some(vec![0, 0, 0, 42]));
604 assert_eq!((-1i32).encode_param(), Some(vec![255, 255, 255, 255]));
605 }
606
607 #[test]
608 fn test_i64_encoding() {
609 assert_eq!(42i64.encode_param(), Some(vec![0, 0, 0, 0, 0, 0, 0, 42]));
610 }
611
612 #[test]
613 fn test_string_encoding() {
614 assert_eq!("hello".encode_param(), Some(b"hello".to_vec()));
615 assert_eq!(
616 String::from("world").encode_param(),
617 Some(b"world".to_vec())
618 );
619 }
620
621 #[test]
622 fn test_bool_encoding() {
623 assert_eq!(true.encode_param(), Some(vec![1]));
624 assert_eq!(false.encode_param(), Some(vec![0]));
625 }
626
627 #[test]
628 fn test_option_encoding() {
629 // Big-endian per PG Bind format code 1.
630 assert_eq!(Some(42i32).encode_param(), Some(vec![0, 0, 0, 42]));
631 assert_eq!(None::<i32>.encode_param(), None);
632 }
633
634 #[test]
635 fn test_reference_encoding() {
636 let value = 42i32;
637 assert_eq!(value.encode_param(), Some(vec![0, 0, 0, 42]));
638 assert_eq!((&&value).encode_param(), Some(vec![0, 0, 0, 42]));
639 }
640
641 #[test]
642 fn test_pg_numeric_encode_unscaled() {
643 // 42 → ndigits=1, weight=0, sign=0, dscale=0, group=42
644 assert_eq!(
645 pg_numeric_encode_unscaled(42),
646 vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 42]
647 );
648
649 // 0 → ndigits=0, weight=0, sign=0, dscale=0 (empty digit list)
650 assert_eq!(pg_numeric_encode_unscaled(0), vec![0, 0, 0, 0, 0, 0, 0, 0]);
651
652 // -1 → ndigits=1, weight=0, sign=0x4000, dscale=0, group=1
653 assert_eq!(
654 pg_numeric_encode_unscaled(-1),
655 vec![0, 1, 0, 0, 0x40, 0, 0, 0, 0, 1]
656 );
657
658 // 123456789 = 1*10000^2 + 2345*10000 + 6789
659 // → ndigits=3, weight=2, sign=0, dscale=0, groups=[1, 2345, 6789]
660 assert_eq!(
661 pg_numeric_encode_unscaled(123_456_789),
662 vec![
663 0, 3, // ndigits=3
664 0, 2, // weight=2
665 0, 0, // sign=0
666 0, 0, // dscale=0
667 0, 1, // group 1
668 9, 41, // group 2345 (0x0929)
669 26, 133 // group 6789 (0x1A85)
670 ]
671 );
672 }
673
674 #[test]
675 fn test_numeric_scale0_encode_param() {
676 // The scale=0 ToSqlParam path produces the canonical whole-number form.
677 assert_eq!(
678 Numeric::new(42, 0).encode_param(),
679 Some(vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 42])
680 );
681 }
682
683 #[test]
684 fn test_numeric_scaled_sets_dscale_for_rejection() {
685 // For scale>0, encode_param sets dscale = true scale so the server
686 // REJECTS the param (0A000). These bytes are intentionally NOT a valid
687 // representation of 1.23 — correct scaled encoding is #132. We only
688 // assert the dscale field (bytes 6..8) carries the scale, which is what
689 // triggers Hyper's fail-fast rejection.
690 let bytes = Numeric::new(123, 2).encode_param().expect("some");
691 assert_eq!(&bytes[6..8], &[0, 2], "dscale must equal the true scale");
692 assert_ne!(&bytes[6..8], &[0, 0], "must not look like a whole number");
693 }
694
695 #[test]
696 fn test_interval_encoding() {
697 // Interval::new(months, days, microseconds)
698 let interval = Interval::new(2, 5, 0);
699 // PG binary: [us:i64 BE][days:i32 BE][months:i32 BE]
700 assert_eq!(
701 interval.encode_param(),
702 Some(vec![
703 0, 0, 0, 0, 0, 0, 0, 0, // us = 0
704 0, 0, 0, 5, // days = 5
705 0, 0, 0, 2 // months = 2
706 ])
707 );
708 }
709
710 #[test]
711 fn test_json_encoding() {
712 let json = serde_json::json!({"a": 1});
713 // UTF-8 bytes of compact JSON string
714 assert_eq!(json.encode_param(), Some(br#"{"a":1}"#.to_vec()));
715 }
716}