Skip to main content

spg_sqlx/types/
array.rs

1//! v7.16.0 — `Type` / `Encode` / `Decode` for the basic array
2//! shapes mailrs uses: `Vec<i32>` (INT[]), `Vec<i64>` (BIGINT[]),
3//! `Vec<String>` (TEXT[]). NULL elements are NOT supported on
4//! Encode (mailrs's existing PG path stores Vec<T>, not
5//! Vec<Option<T>>); Decode tolerates NULLs by skipping the slot.
6//! Round-trip through SPG's text-form `{a,b,c}` external array
7//! shape.
8
9use sqlx_core::decode::Decode;
10use sqlx_core::encode::{Encode, IsNull};
11use sqlx_core::error::BoxDynError;
12use sqlx_core::types::Type;
13
14use spg_embedded::Value as EngineValue;
15
16use crate::arguments::SpgArgumentValue;
17use crate::database::Spg;
18use crate::type_info::{Kind, SpgTypeInfo};
19use crate::value::SpgValueRef;
20
21// ---- [i32] + Vec<i32> / INT[] ----
22//
23// Encode lives on the unsized slice `[T]`; `Vec<T>` delegates and
24// `&[T]` comes for free through sqlx-core's blanket `Encode for &T` +
25// `Type for &T` impls (mailrs binds `= ANY($1)` params as `&[i64]`).
26
27impl Type<Spg> for [i32] {
28    fn type_info() -> SpgTypeInfo {
29        // No dedicated array kind in the v7.16.0 type_info — the
30        // engine stores arrays as IntArray/BigIntArray/TextArray
31        // variants, dispatch happens at coerce time. Surface as
32        // Text so sqlx's type-compatibility check passes for the
33        // PG `INT[]` column type.
34        SpgTypeInfo::of(Kind::Text)
35    }
36    fn compatible(_ty: &SpgTypeInfo) -> bool {
37        true
38    }
39}
40
41impl Type<Spg> for Vec<i32> {
42    fn type_info() -> SpgTypeInfo {
43        <[i32] as Type<Spg>>::type_info()
44    }
45    fn compatible(_ty: &SpgTypeInfo) -> bool {
46        true
47    }
48}
49
50impl<'q> Encode<'q, Spg> for [i32] {
51    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
52        // Native engine array — `= ANY($1)` and INT[] column coerce
53        // both take it directly (the prior `{1,2,3}` text form only
54        // worked through column-typed coercion; ANY() has no column
55        // context — mailrs embed round-12).
56        buf.push(SpgArgumentValue {
57            value: EngineValue::IntArray(self.iter().map(|v| Some(*v)).collect()),
58            type_info: Some(<[i32] as Type<Spg>>::type_info()),
59            _phantom: core::marker::PhantomData,
60        });
61        Ok(IsNull::No)
62    }
63}
64
65impl<'q> Encode<'q, Spg> for Vec<i32> {
66    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
67        self.as_slice().encode_by_ref(buf)
68    }
69}
70
71// sqlx-core's blanket `Encode for &T` requires `T: Sized`, so the
72// borrowed-slice form needs its own impl (delegates to `[i32]`).
73impl<'q> Encode<'q, Spg> for &'q [i32] {
74    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
75        (**self).encode_by_ref(buf)
76    }
77}
78
79impl<'r> Decode<'r, Spg> for Vec<i32> {
80    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
81        match value.engine() {
82            EngineValue::IntArray(items) => Ok(items.iter().filter_map(|o| *o).collect()),
83            // BIGINT[] narrows lossily.
84            EngineValue::BigIntArray(items) => Ok(items
85                .iter()
86                .filter_map(|o| (*o).and_then(|n| i32::try_from(n).ok()))
87                .collect()),
88            other => Err(format!("cannot decode {other:?} as Vec<i32>").into()),
89        }
90    }
91}
92
93// ---- Vec<i64> / BIGINT[] ----
94
95impl Type<Spg> for [i64] {
96    fn type_info() -> SpgTypeInfo {
97        SpgTypeInfo::of(Kind::Text)
98    }
99    fn compatible(_ty: &SpgTypeInfo) -> bool {
100        true
101    }
102}
103
104impl Type<Spg> for Vec<i64> {
105    fn type_info() -> SpgTypeInfo {
106        <[i64] as Type<Spg>>::type_info()
107    }
108    fn compatible(_ty: &SpgTypeInfo) -> bool {
109        true
110    }
111}
112
113impl<'q> Encode<'q, Spg> for [i64] {
114    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
115        buf.push(SpgArgumentValue {
116            value: EngineValue::BigIntArray(self.iter().map(|v| Some(*v)).collect()),
117            type_info: Some(<[i64] as Type<Spg>>::type_info()),
118            _phantom: core::marker::PhantomData,
119        });
120        Ok(IsNull::No)
121    }
122}
123
124impl<'q> Encode<'q, Spg> for Vec<i64> {
125    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
126        self.as_slice().encode_by_ref(buf)
127    }
128}
129
130// sqlx-core's blanket `Encode for &T` requires `T: Sized`, so the
131// borrowed-slice form needs its own impl (delegates to `[i64]`).
132impl<'q> Encode<'q, Spg> for &'q [i64] {
133    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
134        (**self).encode_by_ref(buf)
135    }
136}
137
138impl<'r> Decode<'r, Spg> for Vec<i64> {
139    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
140        match value.engine() {
141            EngineValue::BigIntArray(items) => Ok(items.iter().filter_map(|o| *o).collect()),
142            EngineValue::IntArray(items) => {
143                Ok(items.iter().filter_map(|o| (*o).map(i64::from)).collect())
144            }
145            other => Err(format!("cannot decode {other:?} as Vec<i64>").into()),
146        }
147    }
148}
149
150// ---- Vec<String> / TEXT[] ----
151
152impl Type<Spg> for [String] {
153    fn type_info() -> SpgTypeInfo {
154        SpgTypeInfo::of(Kind::Text)
155    }
156    fn compatible(_ty: &SpgTypeInfo) -> bool {
157        true
158    }
159}
160
161impl Type<Spg> for Vec<String> {
162    fn type_info() -> SpgTypeInfo {
163        <[String] as Type<Spg>>::type_info()
164    }
165    fn compatible(_ty: &SpgTypeInfo) -> bool {
166        true
167    }
168}
169
170/// Native engine text array (see the [i32] note — ANY() needs the
171/// real array value, not the `{"a","b"}` external text form).
172fn encode_text_array<'q, S: AsRef<str>>(
173    items: impl Iterator<Item = S>,
174    buf: &mut Vec<SpgArgumentValue<'q>>,
175) -> Result<IsNull, BoxDynError> {
176    buf.push(SpgArgumentValue {
177        value: EngineValue::TextArray(items.map(|v| Some(v.as_ref().to_string())).collect()),
178        type_info: Some(<[String] as Type<Spg>>::type_info()),
179        _phantom: core::marker::PhantomData,
180    });
181    Ok(IsNull::No)
182}
183
184impl<'q> Encode<'q, Spg> for [String] {
185    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
186        encode_text_array(self.iter(), buf)
187    }
188}
189
190impl<'q> Encode<'q, Spg> for Vec<String> {
191    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
192        self.as_slice().encode_by_ref(buf)
193    }
194}
195
196// sqlx-core's blanket `Encode for &T` requires `T: Sized`, so the
197// borrowed-slice form needs its own impl (delegates to `[String]`).
198impl<'q> Encode<'q, Spg> for &'q [String] {
199    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
200        (**self).encode_by_ref(buf)
201    }
202}
203
204// `&[&str]` / `Vec<&str>` — borrowed text arrays bind too.
205
206impl Type<Spg> for [&str] {
207    fn type_info() -> SpgTypeInfo {
208        SpgTypeInfo::of(Kind::Text)
209    }
210    fn compatible(_ty: &SpgTypeInfo) -> bool {
211        true
212    }
213}
214
215impl Type<Spg> for Vec<&str> {
216    fn type_info() -> SpgTypeInfo {
217        <[&str] as Type<Spg>>::type_info()
218    }
219    fn compatible(_ty: &SpgTypeInfo) -> bool {
220        true
221    }
222}
223
224impl<'q> Encode<'q, Spg> for [&str] {
225    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
226        encode_text_array(self.iter(), buf)
227    }
228}
229
230impl<'q> Encode<'q, Spg> for Vec<&str> {
231    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
232        self.as_slice().encode_by_ref(buf)
233    }
234}
235
236// sqlx-core's blanket `Encode for &T` requires `T: Sized`, so the
237// borrowed-slice form needs its own impl (delegates to `[&str]`).
238impl<'q> Encode<'q, Spg> for &'q [&str] {
239    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
240        (**self).encode_by_ref(buf)
241    }
242}
243
244impl<'r> Decode<'r, Spg> for Vec<String> {
245    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
246        match value.engine() {
247            EngineValue::TextArray(items) => Ok(items.iter().filter_map(|o| o.clone()).collect()),
248            other => Err(format!("cannot decode {other:?} as Vec<String>").into()),
249        }
250    }
251}