elefant-client 0.1.0

A pure rust implementation of a postgres client that is independent of the executor runtime
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
use crate::protocol::FieldDescription;
use crate::types::PostgresType;
use crate::types::{FromSqlBase, FromSqlBinary, FromSqlText, ToSql};
use rust_decimal::Decimal;
use std::error::Error;

// PostgreSQL NUMERIC type - arbitrary precision decimal values
// Binary format: ndigits (i16) + weight (i16) + sign (i16) + dscale (i16) + digits (array of i16)
impl<'a> FromSqlBase<'a> for Decimal {
    fn accepts_postgres_type(oid: i32) -> bool {
        oid == PostgresType::NUMERIC.oid
    }
}

impl<'a> FromSqlBinary<'a> for Decimal {
    fn from_sql_binary(
        raw: &'a [u8],
        _field: &FieldDescription,
    ) -> Result<Self, Box<dyn Error + Sync + Send>> {
        // PostgreSQL NUMERIC binary format:
        // - ndigits (i16): number of digits in the digits array
        // - weight (i16): weight of the first digit (base 10000)
        // - sign (i16): 0x0000 = positive, 0x4000 = negative, 0xC000 = NaN
        // - dscale (i16): display scale (decimal places)
        // - digits (array of i16): base-10000 digits

        if raw.len() < 8 {
            return Err("NUMERIC data too short".into());
        }

        // Parse header fields
        let ndigits = i16::from_be_bytes([raw[0], raw[1]]);
        let weight = i16::from_be_bytes([raw[2], raw[3]]);
        let sign = i16::from_be_bytes([raw[4], raw[5]]);
        let _dscale = i16::from_be_bytes([raw[6], raw[7]]);

        // Check for NaN
        if sign == 0xC000u16 as i16 {
            return Err("NUMERIC NaN values are not supported by rust_decimal".into());
        }

        // Validate sign
        let is_negative = match sign {
            0x0000 => false,
            0x4000 => true,
            _ => return Err(format!("Invalid NUMERIC sign: {sign:#x}").into()),
        };

        // Check we have enough data for all digits
        let expected_len = 8 + (ndigits as usize * 2);
        if raw.len() < expected_len {
            return Err(format!(
                "NUMERIC data too short: expected {} bytes, got {}",
                expected_len,
                raw.len()
            )
            .into());
        }

        // Parse digits (base-10000)
        let mut digits = Vec::with_capacity(ndigits as usize);
        for i in 0..ndigits {
            let offset = 8 + (i as usize * 2);
            let digit = i16::from_be_bytes([raw[offset], raw[offset + 1]]);
            if !(0..10000).contains(&digit) {
                return Err(format!("Invalid NUMERIC digit: {digit}").into());
            }
            digits.push(digit);
        }

        // Convert PostgreSQL base-10000 digits directly to rust_decimal
        // Build up the mantissa value directly without string conversion
        let mut mantissa: i128 = 0;
        let mantissa_scale;

        // Calculate the total decimal places this represents
        let digits_before_decimal = (weight + 1) as i32;
        let total_digit_positions = ndigits as i32;

        if digits_before_decimal <= 0 {
            // All digits are fractional
            // Scale = leading zeros + all digit positions
            mantissa_scale = (-digits_before_decimal * 4 + total_digit_positions * 4) as u32;

            // Build mantissa from the digits
            for &digit in &digits {
                mantissa = mantissa
                    .checked_mul(10000)
                    .and_then(|m| m.checked_add(digit as i128))
                    .ok_or("Numeric value exceeds rust_decimal precision (mantissa overflow)")?;
            }
        } else if digits_before_decimal >= total_digit_positions {
            // All digits are before decimal point
            mantissa_scale = 0;

            // Build mantissa from digits and add trailing zeros
            for &digit in &digits {
                mantissa = mantissa
                    .checked_mul(10000)
                    .and_then(|m| m.checked_add(digit as i128))
                    .ok_or("Numeric value exceeds rust_decimal precision (mantissa overflow)")?;
            }

            // Add trailing zeros for extra weight positions
            let extra_zero_positions = (digits_before_decimal - total_digit_positions) * 4;
            for _ in 0..extra_zero_positions {
                mantissa = mantissa.checked_mul(10).ok_or(
                    "Numeric value exceeds rust_decimal precision (trailing zeros overflow)",
                )?;
            }
        } else {
            // Mixed: some before, some after decimal point
            mantissa_scale = ((total_digit_positions - digits_before_decimal) * 4) as u32;

            // Build mantissa from all digits
            for &digit in &digits {
                mantissa = mantissa
                    .checked_mul(10000)
                    .and_then(|m| m.checked_add(digit as i128))
                    .ok_or("Numeric value exceeds rust_decimal precision (mantissa overflow)")?;
            }
        }

        // Apply sign
        if is_negative {
            mantissa = -mantissa;
        }

        // Create Decimal directly from mantissa and scale
        match Decimal::try_from_i128_with_scale(mantissa, mantissa_scale) {
            Ok(decimal) => Ok(decimal),
            Err(e) => Err(format!("Failed to create Decimal from mantissa {mantissa} with scale {mantissa_scale}: {e}").into()),
        }
    }
}

impl<'a> FromSqlText<'a> for Decimal {
    fn from_sql_text(
        raw: &'a str,
        field: &FieldDescription,
    ) -> Result<Self, Box<dyn Error + Sync + Send>> {
        // NUMERIC text format is standard decimal notation
        match raw.parse::<Decimal>() {
            Ok(decimal) => Ok(decimal),
            Err(e) => Err(format!(
                "Failed to parse NUMERIC from text '{raw}': {e}. Error occurred when parsing field {field:?}"
            ).into()),
        }
    }
}

impl ToSql for Decimal {
    fn to_sql_binary(
        &self,
        target_buffer: &mut Vec<u8>,
    ) -> Result<(), Box<dyn Error + Sync + Send>> {
        // Handle zero case efficiently
        if self.is_zero() {
            target_buffer.extend_from_slice(&0i16.to_be_bytes()); // ndigits
            target_buffer.extend_from_slice(&0i16.to_be_bytes()); // weight
            target_buffer.extend_from_slice(&0i16.to_be_bytes()); // sign (positive)
            target_buffer.extend_from_slice(&0i16.to_be_bytes()); // dscale
            return Ok(());
        }

        // Work directly with the internal representation
        let mantissa = self.mantissa().abs(); // Get absolute mantissa
        let scale = self.scale() as i16; // dscale value
        let is_negative = self.is_sign_negative();

        // The key insight: rust_decimal mantissa represents the number scaled by 10^scale
        // For example: 123.456 has mantissa=123456, scale=3
        // We need to convert this to PostgreSQL's base-10000 representation

        // Step 1: Extract decimal digits from mantissa using arithmetic (no strings!)
        let mut decimal_digits = Vec::new();
        let mut temp_mantissa = mantissa;

        if temp_mantissa == 0 {
            decimal_digits.push(0);
        } else {
            while temp_mantissa > 0 {
                decimal_digits.insert(0, (temp_mantissa % 10) as u8);
                temp_mantissa /= 10;
            }
        }

        // Step 2: Determine how many digits are before decimal point
        let total_decimal_digits = decimal_digits.len() as i16;
        let digits_before_decimal = total_decimal_digits - scale;

        // Step 3: Group decimal digits into base-10000 (4 decimal digits per group)
        let mut digits_10000 = Vec::new();

        // Process integer part (group from right to left to align with base-10000 boundaries)
        if digits_before_decimal > 0 {
            let mut i = digits_before_decimal as usize;
            while i > 0 {
                let start = i.saturating_sub(4);
                let mut group_value: i16 = 0;
                for digit in decimal_digits.iter().take(i).skip(start) {
                    group_value = group_value * 10 + *digit as i16;
                }
                digits_10000.insert(0, group_value);
                i = start;
            }
        }

        // Process fractional part (group from left to right)
        if scale > 0 {
            let fractional_start = if digits_before_decimal > 0 {
                digits_before_decimal as usize
            } else {
                0
            };

            let mut i = fractional_start;
            while i < decimal_digits.len() {
                let end = std::cmp::min(i + 4, decimal_digits.len());
                let mut group_value: i16 = 0;

                // Build the group value
                for digit in decimal_digits.iter().take(end).skip(i) {
                    group_value = group_value * 10 + *digit as i16;
                }

                // Pad with zeros for fractional part (base-10000 groups must represent 4 decimal places)
                let digits_in_group = end - i;
                for _ in digits_in_group..4 {
                    group_value *= 10;
                }

                digits_10000.push(group_value);
                i += 4;
            }
        }

        // Calculate PostgreSQL weight (position of most significant base-10000 digit)
        let weight = if digits_before_decimal <= 0 {
            // Pure fractional number - weight is negative
            let leading_zero_groups = (-digits_before_decimal + 3) / 4;
            -(leading_zero_groups + 1)
        } else {
            // Has integer part
            ((digits_before_decimal + 3) / 4) - 1
        };

        // Remove trailing zero digits (but preserve at least one digit for fractional numbers)
        while digits_10000.len() > 1 && digits_10000.last() == Some(&0) {
            digits_10000.pop();
        }

        let ndigits = digits_10000.len() as i16;
        let sign = if is_negative {
            0x4000u16 as i16
        } else {
            0x0000i16
        };

        // Write PostgreSQL NUMERIC binary format
        target_buffer.extend_from_slice(&ndigits.to_be_bytes());
        target_buffer.extend_from_slice(&weight.to_be_bytes());
        target_buffer.extend_from_slice(&sign.to_be_bytes());
        target_buffer.extend_from_slice(&scale.to_be_bytes()); // dscale

        for digit in digits_10000 {
            target_buffer.extend_from_slice(&digit.to_be_bytes());
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use rust_decimal::Decimal;

    #[cfg(feature = "tokio")]
    mod tokio_connection {
        use super::*;
        use crate::test_helpers::get_settings;
        use crate::tokio_connection::new_client;
        use tokio::test;

        #[test]
        async fn test_numeric_basic_values() {
            let mut client = new_client(get_settings()).await.unwrap();

            // Test zero
            let zero: Decimal = client
                .read_single_value_dual_mode("select 0::numeric")
                .await;
            assert_eq!(zero, Decimal::from(0));

            // Test positive integer
            let positive: Decimal = client
                .read_single_value_dual_mode("select 12345::numeric")
                .await;
            assert_eq!(positive, Decimal::from(12345));

            // Test negative integer
            let negative: Decimal = client
                .read_single_value_dual_mode("select -67890::numeric")
                .await;
            assert_eq!(negative, Decimal::from(-67890));

            // Test decimal
            let decimal: Decimal = client
                .read_single_value_dual_mode("select 123.456::numeric")
                .await;
            assert_eq!(decimal, "123.456".parse::<Decimal>().unwrap());
        }

        #[test]
        async fn test_numeric_precision_scale() {
            let mut client = new_client(get_settings()).await.unwrap();

            // Test high precision
            let high_precision: Decimal = client
                .read_single_value_dual_mode::<Decimal>("select 123456789.123456789::numeric(18,9)")
                .await;
            assert_eq!(
                high_precision,
                "123456789.123456789".parse::<Decimal>().unwrap()
            );

            // Test many decimal places
            let many_decimals: Decimal = client
                .read_single_value_dual_mode::<Decimal>("select 1.000000001::numeric(10,9)")
                .await;
            assert_eq!(many_decimals, "1.000000001".parse::<Decimal>().unwrap());

            // Test large integer
            let large_int: Decimal = client
                .read_single_value_dual_mode("select 999999999999999999::numeric")
                .await;
            assert_eq!(large_int, "999999999999999999".parse::<Decimal>().unwrap());
        }

        #[test]
        async fn test_numeric_postgresql_direct() {
            let mut client = new_client(get_settings()).await.unwrap();

            // Test what PostgreSQL actually sends for small decimals
            let small_decimal: Decimal = client
                .read_single_value_dual_mode("select 0.000000001::numeric")
                .await;

            let expected = "0.000000001".parse::<Decimal>().unwrap();
            assert_eq!(small_decimal, expected, "Direct PostgreSQL read failed");
        }

        #[test]
        async fn test_numeric_small_decimal_round_trip() {
            let mut client = new_client(get_settings()).await.unwrap();

            client.execute_non_query_simple("drop table if exists test_numeric_debug; create table test_numeric_debug(value numeric);").await.unwrap();

            // Test the failing case
            let test_value = "0.000000001".parse::<Decimal>().unwrap();

            client
                .execute_non_query(
                    "insert into test_numeric_debug values ($1);",
                    &[&test_value],
                )
                .await
                .unwrap();

            let retrieved: Decimal = client
                .read_single_value_dual_mode("select value from test_numeric_debug")
                .await;

            assert_eq!(
                retrieved, test_value,
                "Small decimal round-trip failed for {test_value}"
            );
        }

        #[test]
        async fn test_numeric_round_trip() {
            let mut client = new_client(get_settings()).await.unwrap();

            client.execute_non_query_simple("drop table if exists test_numeric_table; create table test_numeric_table(value numeric);").await.unwrap();

            let test_values = vec![
                "0".parse::<Decimal>().unwrap(),
                "123.456".parse::<Decimal>().unwrap(),
                "-789.012".parse::<Decimal>().unwrap(),
                "999999999.999999999".parse::<Decimal>().unwrap(),
                "0.000000001".parse::<Decimal>().unwrap(),
                "1000000000".parse::<Decimal>().unwrap(),
            ];

            for test_value in &test_values {
                client
                    .execute_non_query("insert into test_numeric_table values ($1);", &[test_value])
                    .await
                    .unwrap();

                let retrieved: Decimal = client
                    .read_single_value(
                        "select value from test_numeric_table order by value desc limit 1;",
                        &[],
                    )
                    .await;

                assert_eq!(&retrieved, test_value, "Round-trip failed for {test_value}");

                // Clean up for next iteration
                client
                    .execute_non_query("delete from test_numeric_table;", &[])
                    .await
                    .unwrap();
            }
        }

        #[test]
        async fn test_numeric_null_handling() {
            let mut client = new_client(get_settings()).await.unwrap();

            let null_value: Option<Decimal> = client
                .read_single_value_dual_mode("select null::numeric")
                .await;
            assert_eq!(null_value, None);
        }

        #[test]
        async fn test_numeric_edge_cases() {
            let mut client = new_client(get_settings()).await.unwrap();

            // Test very small number
            let small: Decimal = client
                .read_single_value_dual_mode("select 0.0001::numeric")
                .await;
            assert_eq!(small, "0.0001".parse::<Decimal>().unwrap());

            // Test number with trailing zeros
            let trailing_zeros: Decimal = client
                .read_single_value_dual_mode("select 123.4500::numeric")
                .await;
            assert_eq!(trailing_zeros, "123.45".parse::<Decimal>().unwrap()); // PostgreSQL should normalize
        }

        #[test]
        async fn test_numeric_nan_error() {
            let mut client = new_client(get_settings()).await.unwrap();

            // Test NaN should return an error
            let nan_result = client
                .try_read_single_value::<Decimal>("select 'NaN'::numeric;", &[])
                .await;
            assert!(nan_result.is_err(), "Expected error for NaN NUMERIC");
            assert!(nan_result.unwrap_err().to_string().contains("NaN"));
        }

        #[test]
        async fn test_numeric_array_support() {
            let mut client = new_client(get_settings()).await.unwrap();

            // Test that reading numeric arrays works (PostgreSQL arrays automatically supported)
            let numeric_array: Vec<Decimal> = client
                .read_single_value_dual_mode("select ARRAY[0::numeric, 123.456::numeric, -789.012::numeric, 0.000000001::numeric]").await;

            let expected = vec![
                "0".parse::<Decimal>().unwrap(),
                "123.456".parse::<Decimal>().unwrap(),
                "-789.012".parse::<Decimal>().unwrap(),
                "0.000000001".parse::<Decimal>().unwrap(),
            ];

            assert_eq!(numeric_array, expected);
        }
    }
}