Skip to main content

sqlmodel_postgres/types/
encode.rs

1//! PostgreSQL type encoding (Rust → PostgreSQL).
2//!
3//! This module provides traits and implementations for encoding Rust values
4//! to PostgreSQL wire format in both text and binary representations.
5
6// The Error type is intentionally large to provide rich error context.
7// This is a design decision made at the workspace level.
8#![allow(clippy::result_large_err)]
9// Truncation is expected when converting between timestamp types
10#![allow(clippy::cast_possible_truncation)]
11
12use sqlmodel_core::Error;
13use sqlmodel_core::error::TypeError;
14
15use super::oid;
16
17/// Wire format for PostgreSQL values.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum Format {
20    /// Text format (human-readable strings)
21    #[default]
22    Text,
23    /// Binary format (PostgreSQL native binary representation)
24    Binary,
25}
26
27impl Format {
28    /// Get the format code for the wire protocol (0 = text, 1 = binary).
29    #[must_use]
30    pub const fn code(self) -> i16 {
31        match self {
32            Format::Text => 0,
33            Format::Binary => 1,
34        }
35    }
36
37    /// Create format from wire protocol code.
38    #[must_use]
39    pub const fn from_code(code: i16) -> Self {
40        match code {
41            1 => Format::Binary,
42            _ => Format::Text,
43        }
44    }
45}
46
47/// Encode a value to PostgreSQL text format.
48pub trait TextEncode {
49    /// Encode self to a text string for PostgreSQL.
50    fn encode_text(&self) -> String;
51}
52
53/// Encode a value to PostgreSQL binary format.
54pub trait BinaryEncode {
55    /// Encode self to binary bytes for PostgreSQL.
56    fn encode_binary(&self, buf: &mut Vec<u8>);
57}
58
59/// Combined encoding trait that supports both formats.
60pub trait Encode: TextEncode + BinaryEncode {
61    /// Get the PostgreSQL OID for this type.
62    fn oid() -> u32;
63
64    /// Encode to the specified format.
65    fn encode(&self, format: Format, buf: &mut Vec<u8>) {
66        match format {
67            Format::Text => buf.extend(self.encode_text().as_bytes()),
68            Format::Binary => self.encode_binary(buf),
69        }
70    }
71}
72
73// ==================== Boolean ====================
74
75impl TextEncode for bool {
76    fn encode_text(&self) -> String {
77        if *self { "t" } else { "f" }.to_string()
78    }
79}
80
81impl BinaryEncode for bool {
82    fn encode_binary(&self, buf: &mut Vec<u8>) {
83        buf.push(u8::from(*self));
84    }
85}
86
87impl Encode for bool {
88    fn oid() -> u32 {
89        oid::BOOL
90    }
91}
92
93// ==================== Integers ====================
94
95impl TextEncode for i8 {
96    fn encode_text(&self) -> String {
97        self.to_string()
98    }
99}
100
101impl BinaryEncode for i8 {
102    fn encode_binary(&self, buf: &mut Vec<u8>) {
103        buf.push(*self as u8);
104    }
105}
106
107impl TextEncode for i16 {
108    fn encode_text(&self) -> String {
109        self.to_string()
110    }
111}
112
113impl BinaryEncode for i16 {
114    fn encode_binary(&self, buf: &mut Vec<u8>) {
115        buf.extend_from_slice(&self.to_be_bytes());
116    }
117}
118
119impl Encode for i16 {
120    fn oid() -> u32 {
121        oid::INT2
122    }
123}
124
125impl TextEncode for i32 {
126    fn encode_text(&self) -> String {
127        self.to_string()
128    }
129}
130
131impl BinaryEncode for i32 {
132    fn encode_binary(&self, buf: &mut Vec<u8>) {
133        buf.extend_from_slice(&self.to_be_bytes());
134    }
135}
136
137impl Encode for i32 {
138    fn oid() -> u32 {
139        oid::INT4
140    }
141}
142
143impl TextEncode for i64 {
144    fn encode_text(&self) -> String {
145        self.to_string()
146    }
147}
148
149impl BinaryEncode for i64 {
150    fn encode_binary(&self, buf: &mut Vec<u8>) {
151        buf.extend_from_slice(&self.to_be_bytes());
152    }
153}
154
155impl Encode for i64 {
156    fn oid() -> u32 {
157        oid::INT8
158    }
159}
160
161// ==================== Unsigned Integers ====================
162// PostgreSQL doesn't have unsigned types, so we encode as the next larger signed type
163
164impl TextEncode for u32 {
165    fn encode_text(&self) -> String {
166        // Encode as i64 to avoid overflow
167        i64::from(*self).to_string()
168    }
169}
170
171impl BinaryEncode for u32 {
172    fn encode_binary(&self, buf: &mut Vec<u8>) {
173        // Encode as i64
174        i64::from(*self).encode_binary(buf);
175    }
176}
177
178// ==================== Floating Point ====================
179
180impl TextEncode for f32 {
181    fn encode_text(&self) -> String {
182        if self.is_nan() {
183            "NaN".to_string()
184        } else if self.is_infinite() {
185            if self.is_sign_positive() {
186                "Infinity".to_string()
187            } else {
188                "-Infinity".to_string()
189            }
190        } else {
191            self.to_string()
192        }
193    }
194}
195
196impl BinaryEncode for f32 {
197    fn encode_binary(&self, buf: &mut Vec<u8>) {
198        buf.extend_from_slice(&self.to_be_bytes());
199    }
200}
201
202impl Encode for f32 {
203    fn oid() -> u32 {
204        oid::FLOAT4
205    }
206}
207
208impl TextEncode for f64 {
209    fn encode_text(&self) -> String {
210        if self.is_nan() {
211            "NaN".to_string()
212        } else if self.is_infinite() {
213            if self.is_sign_positive() {
214                "Infinity".to_string()
215            } else {
216                "-Infinity".to_string()
217            }
218        } else {
219            self.to_string()
220        }
221    }
222}
223
224impl BinaryEncode for f64 {
225    fn encode_binary(&self, buf: &mut Vec<u8>) {
226        buf.extend_from_slice(&self.to_be_bytes());
227    }
228}
229
230impl Encode for f64 {
231    fn oid() -> u32 {
232        oid::FLOAT8
233    }
234}
235
236// ==================== Strings ====================
237
238impl TextEncode for str {
239    fn encode_text(&self) -> String {
240        self.to_string()
241    }
242}
243
244impl BinaryEncode for str {
245    fn encode_binary(&self, buf: &mut Vec<u8>) {
246        buf.extend_from_slice(self.as_bytes());
247    }
248}
249
250impl TextEncode for String {
251    fn encode_text(&self) -> String {
252        self.clone()
253    }
254}
255
256impl BinaryEncode for String {
257    fn encode_binary(&self, buf: &mut Vec<u8>) {
258        buf.extend_from_slice(self.as_bytes());
259    }
260}
261
262impl Encode for String {
263    fn oid() -> u32 {
264        oid::TEXT
265    }
266}
267
268impl TextEncode for &str {
269    fn encode_text(&self) -> String {
270        (*self).to_string()
271    }
272}
273
274impl BinaryEncode for &str {
275    fn encode_binary(&self, buf: &mut Vec<u8>) {
276        buf.extend_from_slice(self.as_bytes());
277    }
278}
279
280// ==================== Bytes ====================
281
282impl TextEncode for [u8] {
283    fn encode_text(&self) -> String {
284        // Encode as hex with \x prefix
285        let mut s = String::with_capacity(2 + self.len() * 2);
286        s.push_str("\\x");
287        for byte in self {
288            s.push_str(&format!("{byte:02x}"));
289        }
290        s
291    }
292}
293
294impl BinaryEncode for [u8] {
295    fn encode_binary(&self, buf: &mut Vec<u8>) {
296        buf.extend_from_slice(self);
297    }
298}
299
300impl TextEncode for Vec<u8> {
301    fn encode_text(&self) -> String {
302        self.as_slice().encode_text()
303    }
304}
305
306impl BinaryEncode for Vec<u8> {
307    fn encode_binary(&self, buf: &mut Vec<u8>) {
308        buf.extend_from_slice(self);
309    }
310}
311
312impl Encode for Vec<u8> {
313    fn oid() -> u32 {
314        oid::BYTEA
315    }
316}
317
318// ==================== UUID ====================
319
320/// Encode a UUID (16-byte array) to PostgreSQL format.
321impl TextEncode for [u8; 16] {
322    fn encode_text(&self) -> String {
323        format!(
324            "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
325            self[0],
326            self[1],
327            self[2],
328            self[3],
329            self[4],
330            self[5],
331            self[6],
332            self[7],
333            self[8],
334            self[9],
335            self[10],
336            self[11],
337            self[12],
338            self[13],
339            self[14],
340            self[15]
341        )
342    }
343}
344
345impl BinaryEncode for [u8; 16] {
346    fn encode_binary(&self, buf: &mut Vec<u8>) {
347        buf.extend_from_slice(self);
348    }
349}
350
351// ==================== Date/Time ====================
352
353/// Days since Unix epoch (1970-01-01).
354/// PostgreSQL uses 2000-01-01 as its epoch, so we need to convert.
355const PG_EPOCH_OFFSET_DAYS: i32 = 10_957; // Days from 1970-01-01 to 2000-01-01
356
357/// Microseconds since Unix epoch for timestamp.
358/// PostgreSQL uses 2000-01-01 as its epoch.
359const PG_EPOCH_OFFSET_MICROS: i64 = 946_684_800_000_000; // Micros from 1970 to 2000
360
361/// Encode a date as days since Unix epoch.
362///
363/// Input is days since 1970-01-01, output is days since 2000-01-01.
364pub fn encode_date_days(days_since_unix: i32) -> i32 {
365    days_since_unix - PG_EPOCH_OFFSET_DAYS
366}
367
368/// Encode a timestamp as microseconds since Unix epoch.
369///
370/// Input is microseconds since 1970-01-01 00:00:00 UTC.
371/// Output is microseconds since 2000-01-01 00:00:00 UTC (PostgreSQL epoch).
372pub fn encode_timestamp_micros(micros_since_unix: i64) -> i64 {
373    micros_since_unix - PG_EPOCH_OFFSET_MICROS
374}
375
376/// Encode a time as microseconds since midnight.
377pub fn encode_time_micros(micros_since_midnight: i64) -> i64 {
378    micros_since_midnight
379}
380
381// ==================== Optional Values ====================
382
383impl<T: TextEncode> TextEncode for Option<T> {
384    fn encode_text(&self) -> String {
385        match self {
386            Some(v) => v.encode_text(),
387            None => String::new(),
388        }
389    }
390}
391
392impl<T: BinaryEncode> BinaryEncode for Option<T> {
393    fn encode_binary(&self, buf: &mut Vec<u8>) {
394        if let Some(v) = self {
395            v.encode_binary(buf);
396        }
397    }
398}
399
400// ==================== Value Encoding ====================
401
402use sqlmodel_core::value::Value;
403
404/// Encode a dynamic Value to the specified format.
405///
406/// Returns the encoded bytes and the appropriate OID.
407pub fn encode_value(value: &Value, format: Format) -> Result<(Vec<u8>, u32), Error> {
408    let mut buf = Vec::new();
409    let type_oid = match value {
410        Value::Null => return Ok((vec![], oid::UNKNOWN)),
411        Value::Bool(v) => {
412            match format {
413                Format::Text => buf.extend(v.encode_text().as_bytes()),
414                Format::Binary => v.encode_binary(&mut buf),
415            }
416            oid::BOOL
417        }
418        Value::TinyInt(v) => {
419            match format {
420                Format::Text => buf.extend(v.encode_text().as_bytes()),
421                Format::Binary => {
422                    // PostgreSQL doesn't have int1, encode as int2
423                    i16::from(*v).encode_binary(&mut buf);
424                }
425            }
426            oid::INT2
427        }
428        Value::SmallInt(v) => {
429            match format {
430                Format::Text => buf.extend(v.encode_text().as_bytes()),
431                Format::Binary => v.encode_binary(&mut buf),
432            }
433            oid::INT2
434        }
435        Value::Int(v) => {
436            match format {
437                Format::Text => buf.extend(v.encode_text().as_bytes()),
438                Format::Binary => v.encode_binary(&mut buf),
439            }
440            oid::INT4
441        }
442        Value::BigInt(v) => {
443            match format {
444                Format::Text => buf.extend(v.encode_text().as_bytes()),
445                Format::Binary => v.encode_binary(&mut buf),
446            }
447            oid::INT8
448        }
449        Value::Float(v) => {
450            match format {
451                Format::Text => buf.extend(v.encode_text().as_bytes()),
452                Format::Binary => v.encode_binary(&mut buf),
453            }
454            oid::FLOAT4
455        }
456        Value::Double(v) => {
457            match format {
458                Format::Text => buf.extend(v.encode_text().as_bytes()),
459                Format::Binary => v.encode_binary(&mut buf),
460            }
461            oid::FLOAT8
462        }
463        Value::Decimal(v) => {
464            buf.extend(v.as_bytes());
465            oid::NUMERIC
466        }
467        Value::Text(v) => {
468            buf.extend(v.as_bytes());
469            oid::TEXT
470        }
471        Value::Bytes(v) => {
472            match format {
473                Format::Text => buf.extend(v.encode_text().as_bytes()),
474                Format::Binary => v.encode_binary(&mut buf),
475            }
476            oid::BYTEA
477        }
478        Value::Date(days) => {
479            match format {
480                Format::Text => {
481                    // Convert days since Unix epoch to YYYY-MM-DD
482                    let date = days_to_date_string(*days);
483                    buf.extend(date.as_bytes());
484                }
485                Format::Binary => {
486                    encode_date_days(*days).encode_binary(&mut buf);
487                }
488            }
489            oid::DATE
490        }
491        Value::Time(micros) => {
492            match format {
493                Format::Text => {
494                    let time = micros_to_time_string(*micros);
495                    buf.extend(time.as_bytes());
496                }
497                Format::Binary => {
498                    micros.encode_binary(&mut buf);
499                }
500            }
501            oid::TIME
502        }
503        Value::Timestamp(micros) => {
504            match format {
505                Format::Text => {
506                    let ts = micros_to_timestamp_string(*micros);
507                    buf.extend(ts.as_bytes());
508                }
509                Format::Binary => {
510                    encode_timestamp_micros(*micros).encode_binary(&mut buf);
511                }
512            }
513            oid::TIMESTAMP
514        }
515        Value::TimestampTz(micros) => {
516            match format {
517                Format::Text => {
518                    let ts = micros_to_timestamp_string(*micros);
519                    buf.extend(ts.as_bytes());
520                    buf.extend(b"+00");
521                }
522                Format::Binary => {
523                    encode_timestamp_micros(*micros).encode_binary(&mut buf);
524                }
525            }
526            oid::TIMESTAMPTZ
527        }
528        Value::Uuid(bytes) => {
529            match format {
530                Format::Text => buf.extend(bytes.encode_text().as_bytes()),
531                Format::Binary => bytes.encode_binary(&mut buf),
532            }
533            oid::UUID
534        }
535        Value::Json(json) => {
536            buf.extend(json.to_string().as_bytes());
537            oid::JSON
538        }
539        Value::Array(values) => {
540            return Err(Error::Type(TypeError {
541                expected: "scalar value",
542                actual: format!("array with {} elements", values.len()),
543                column: None,
544                rust_type: None,
545            }));
546        }
547        Value::Default => return Ok((vec![], oid::UNKNOWN)),
548    };
549
550    Ok((buf, type_oid))
551}
552
553// ==================== Helper Functions ====================
554
555/// Convert days since Unix epoch to YYYY-MM-DD string.
556#[allow(clippy::many_single_char_names)]
557fn days_to_date_string(days: i32) -> String {
558    // Julian day conversion algorithm - variable names follow the standard algorithm
559    // Reference: https://howardhinnant.github.io/date_algorithms.html
560    let unix_epoch_jd = 2_440_588; // Julian day of 1970-01-01
561    let jd = unix_epoch_jd + i64::from(days);
562
563    // Julian day to Gregorian date conversion
564    let l = jd + 68_569;
565    let n = 4 * l / 146_097;
566    let l = l - (146_097 * n + 3) / 4;
567    let i = 4000 * (l + 1) / 1_461_001;
568    let l = l - 1461 * i / 4 + 31;
569    let j = 80 * l / 2447;
570    let d = l - 2447 * j / 80;
571    let l = j / 11;
572    let m = j + 2 - 12 * l;
573    let y = 100 * (n - 49) + i + l;
574
575    format!("{y:04}-{m:02}-{d:02}")
576}
577
578/// Convert microseconds since midnight to HH:MM:SS.ffffff string.
579fn micros_to_time_string(micros: i64) -> String {
580    let total_secs = micros / 1_000_000;
581    let frac_micros = micros % 1_000_000;
582    let hours = total_secs / 3600;
583    let mins = (total_secs % 3600) / 60;
584    let secs = total_secs % 60;
585
586    if frac_micros == 0 {
587        format!("{hours:02}:{mins:02}:{secs:02}")
588    } else {
589        format!("{hours:02}:{mins:02}:{secs:02}.{frac_micros:06}")
590    }
591}
592
593/// Convert microseconds since Unix epoch to timestamp string.
594fn micros_to_timestamp_string(micros: i64) -> String {
595    let days = micros / (86_400 * 1_000_000);
596    let day_micros = micros % (86_400 * 1_000_000);
597
598    let date = days_to_date_string(days as i32);
599    let time = micros_to_time_string(day_micros);
600
601    format!("{date} {time}")
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn test_bool_encoding() {
610        assert_eq!(true.encode_text(), "t");
611        assert_eq!(false.encode_text(), "f");
612
613        let mut buf = Vec::new();
614        true.encode_binary(&mut buf);
615        assert_eq!(buf, vec![1]);
616
617        buf.clear();
618        false.encode_binary(&mut buf);
619        assert_eq!(buf, vec![0]);
620    }
621
622    #[test]
623    fn test_integer_encoding() {
624        assert_eq!(42i32.encode_text(), "42");
625        assert_eq!((-100i64).encode_text(), "-100");
626
627        let mut buf = Vec::new();
628        42i32.encode_binary(&mut buf);
629        assert_eq!(buf, vec![0, 0, 0, 42]);
630
631        buf.clear();
632        256i32.encode_binary(&mut buf);
633        assert_eq!(buf, vec![0, 0, 1, 0]);
634    }
635
636    #[test]
637    fn test_float_encoding() {
638        assert_eq!(f64::NAN.encode_text(), "NaN");
639        assert_eq!(f64::INFINITY.encode_text(), "Infinity");
640        assert_eq!(f64::NEG_INFINITY.encode_text(), "-Infinity");
641    }
642
643    #[test]
644    fn test_bytea_encoding() {
645        let bytes = vec![0xDE, 0xAD, 0xBE, 0xEF];
646        assert_eq!(bytes.encode_text(), "\\xdeadbeef");
647    }
648
649    #[test]
650    fn test_uuid_encoding() {
651        let uuid: [u8; 16] = [
652            0x55, 0x06, 0x9c, 0x47, 0x86, 0x8b, 0x4a, 0x08, 0xa4, 0x7f, 0x36, 0x53, 0x26, 0x2b,
653            0xce, 0x35,
654        ];
655        assert_eq!(uuid.encode_text(), "55069c47-868b-4a08-a47f-3653262bce35");
656    }
657
658    #[test]
659    fn test_format_code() {
660        assert_eq!(Format::Text.code(), 0);
661        assert_eq!(Format::Binary.code(), 1);
662        assert_eq!(Format::from_code(0), Format::Text);
663        assert_eq!(Format::from_code(1), Format::Binary);
664    }
665}