1use std::str::FromStr;
17
18use nautilus_model::enums::{
19 AggregationSource, AggressorSide, AssetClass, BarAggregation, CurrencyType, PriceType,
20 TrailingOffsetType,
21};
22use sqlx::{
23 Database, Decode, Postgres, encode::IsNull, error::BoxDynError, postgres::PgTypeInfo,
24 types::Type,
25};
26
27#[derive(Debug)]
28pub struct CurrencyTypePg(pub CurrencyType);
29
30#[derive(Debug)]
31pub struct PriceTypePg(pub PriceType);
32
33#[derive(Debug)]
34pub struct BarAggregationPg(pub BarAggregation);
35
36#[derive(Debug)]
37pub struct AssetClassPg(pub AssetClass);
38
39#[derive(Debug)]
40pub struct TrailingOffsetTypePg(pub Option<TrailingOffsetType>);
41
42#[derive(Debug)]
43pub struct AggressorSidePg(pub AggressorSide);
44
45#[derive(Debug)]
46pub struct AggregationSourcePg(pub AggregationSource);
47
48impl sqlx::Encode<'_, sqlx::Postgres> for CurrencyTypePg {
49 fn encode_by_ref(
50 &self,
51 buf: &mut <Postgres as Database>::ArgumentBuffer,
52 ) -> Result<IsNull, BoxDynError> {
53 let currency_type_str = match self.0 {
54 CurrencyType::Crypto => "CRYPTO",
55 CurrencyType::Fiat => "FIAT",
56 CurrencyType::CommodityBacked => "COMMODITY_BACKED",
57 };
58 <&str as sqlx::Encode<sqlx::Postgres>>::encode(currency_type_str, buf)
59 }
60}
61
62impl<'r> sqlx::Decode<'r, sqlx::Postgres> for CurrencyTypePg {
63 fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
64 let currency_type_str: &str = <&str as Decode<sqlx::Postgres>>::decode(value)?;
65 let currency_type = CurrencyType::from_str(currency_type_str).map_err(|_| {
66 sqlx::Error::Decode(format!("Invalid currency type: {currency_type_str}").into())
67 })?;
68 Ok(Self(currency_type))
69 }
70}
71
72impl sqlx::Type<sqlx::Postgres> for CurrencyTypePg {
73 fn type_info() -> sqlx::postgres::PgTypeInfo {
74 PgTypeInfo::with_name("currency_type")
75 }
76
77 fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
78 *ty == Self::type_info() || <&str as Type<sqlx::Postgres>>::compatible(ty)
79 }
80}
81
82impl sqlx::Encode<'_, sqlx::Postgres> for AssetClassPg {
83 fn encode_by_ref(
84 &self,
85 buf: &mut <Postgres as Database>::ArgumentBuffer,
86 ) -> Result<IsNull, BoxDynError> {
87 let asset_type_str = match self.0 {
88 AssetClass::FX => "FX",
89 AssetClass::Equity => "EQUITY",
90 AssetClass::Commodity => "COMMODITY",
91 AssetClass::Debt => "DEBT",
92 AssetClass::Index => "INDEX",
93 AssetClass::Cryptocurrency => "CRYPTOCURRENCY",
94 AssetClass::Alternative => "ALTERNATIVE",
95 };
96 <&str as sqlx::Encode<sqlx::Postgres>>::encode(asset_type_str, buf)
97 }
98}
99
100impl<'r> sqlx::Decode<'r, sqlx::Postgres> for AssetClassPg {
101 fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
102 let asset_class_str: &str = <&str as Decode<sqlx::Postgres>>::decode(value)?;
103 let asset_class = AssetClass::from_str(asset_class_str).map_err(|_| {
104 sqlx::Error::Decode(format!("Invalid asset class: {asset_class_str}").into())
105 })?;
106 Ok(Self(asset_class))
107 }
108}
109
110impl sqlx::Type<sqlx::Postgres> for AssetClassPg {
111 fn type_info() -> sqlx::postgres::PgTypeInfo {
112 PgTypeInfo::with_name("asset_class")
113 }
114
115 fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
116 *ty == Self::type_info() || <&str as Type<sqlx::Postgres>>::compatible(ty)
117 }
118}
119
120impl sqlx::Encode<'_, sqlx::Postgres> for TrailingOffsetTypePg {
121 fn encode_by_ref(
122 &self,
123 buf: &mut <Postgres as Database>::ArgumentBuffer,
124 ) -> Result<IsNull, BoxDynError> {
125 let value = self.0.as_ref().map_or("NO_TRAILING_OFFSET", AsRef::as_ref);
126 <&str as sqlx::Encode<sqlx::Postgres>>::encode(value, buf)
127 }
128}
129
130impl<'r> sqlx::Decode<'r, sqlx::Postgres> for TrailingOffsetTypePg {
131 fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
132 let trailing_offset_type_str: &str = <&str as Decode<sqlx::Postgres>>::decode(value)?;
133 let trailing_offset_type = if trailing_offset_type_str == "NO_TRAILING_OFFSET" {
134 None
135 } else {
136 Some(
137 TrailingOffsetType::from_str(trailing_offset_type_str).map_err(|_| {
138 sqlx::Error::Decode(
139 format!("Invalid trailing offset type: {trailing_offset_type_str}").into(),
140 )
141 })?,
142 )
143 };
144 Ok(Self(trailing_offset_type))
145 }
146}
147
148impl sqlx::Type<sqlx::Postgres> for TrailingOffsetTypePg {
149 fn type_info() -> sqlx::postgres::PgTypeInfo {
150 PgTypeInfo::with_name("trailing_offset_type")
151 }
152
153 fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
154 *ty == Self::type_info() || <&str as Type<sqlx::Postgres>>::compatible(ty)
155 }
156}
157
158impl sqlx::Encode<'_, sqlx::Postgres> for AggressorSidePg {
159 fn encode_by_ref(
160 &self,
161 buf: &mut <Postgres as Database>::ArgumentBuffer,
162 ) -> Result<IsNull, BoxDynError> {
163 let aggressor_side_str = match self.0 {
164 AggressorSide::NoAggressor => "NO_AGGRESSOR",
165 AggressorSide::Buy => "BUY",
166 AggressorSide::Sell => "SELL",
167 };
168 <&str as sqlx::Encode<sqlx::Postgres>>::encode(aggressor_side_str, buf)
169 }
170}
171
172impl<'r> sqlx::Decode<'r, sqlx::Postgres> for AggressorSidePg {
173 fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
174 let aggressor_side_str: &str = <&str as Decode<sqlx::Postgres>>::decode(value)?;
175 let aggressor_side = AggressorSide::from_str(aggressor_side_str).map_err(|_| {
176 sqlx::Error::Decode(format!("Invalid aggressor side: {aggressor_side_str}").into())
177 })?;
178 Ok(Self(aggressor_side))
179 }
180}
181
182impl sqlx::Type<sqlx::Postgres> for AggressorSidePg {
183 fn type_info() -> sqlx::postgres::PgTypeInfo {
184 PgTypeInfo::with_name("aggressor_side")
185 }
186
187 fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
188 *ty == Self::type_info() || <&str as Type<sqlx::Postgres>>::compatible(ty)
189 }
190}
191
192impl sqlx::Encode<'_, sqlx::Postgres> for AggregationSourcePg {
193 fn encode_by_ref(
194 &self,
195 buf: &mut <Postgres as Database>::ArgumentBuffer,
196 ) -> Result<IsNull, BoxDynError> {
197 let aggregation_source_str = match self.0 {
198 AggregationSource::Internal => "INTERNAL",
199 AggregationSource::External => "EXTERNAL",
200 };
201 <&str as sqlx::Encode<sqlx::Postgres>>::encode(aggregation_source_str, buf)
202 }
203}
204
205impl<'r> sqlx::Decode<'r, sqlx::Postgres> for AggregationSourcePg {
206 fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
207 let aggregation_source_str: &str = <&str as Decode<sqlx::Postgres>>::decode(value)?;
208 let aggregation_source =
209 AggregationSource::from_str(aggregation_source_str).map_err(|_| {
210 sqlx::Error::Decode(
211 format!("Invalid aggregation source: {aggregation_source_str}").into(),
212 )
213 })?;
214 Ok(Self(aggregation_source))
215 }
216}
217
218impl sqlx::Type<sqlx::Postgres> for AggregationSourcePg {
219 fn type_info() -> sqlx::postgres::PgTypeInfo {
220 PgTypeInfo::with_name("aggregation_source")
221 }
222
223 fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
224 *ty == Self::type_info() || <&str as Type<sqlx::Postgres>>::compatible(ty)
225 }
226}
227
228impl sqlx::Encode<'_, sqlx::Postgres> for BarAggregationPg {
229 fn encode_by_ref(
230 &self,
231 buf: &mut <Postgres as Database>::ArgumentBuffer,
232 ) -> Result<IsNull, BoxDynError> {
233 let bar_aggregation_str = match self.0 {
234 BarAggregation::Tick => "TICK",
235 BarAggregation::TickImbalance => "TICK_IMBALANCE",
236 BarAggregation::TickRuns => "TICK_RUNS",
237 BarAggregation::Volume => "VOLUME",
238 BarAggregation::VolumeImbalance => "VOLUME_IMBALANCE",
239 BarAggregation::VolumeRuns => "VOLUME_RUNS",
240 BarAggregation::Value => "VALUE",
241 BarAggregation::ValueImbalance => "VALUE_IMBALANCE",
242 BarAggregation::ValueRuns => "VALUE_RUNS",
243 BarAggregation::Millisecond => "MILLISECOND",
244 BarAggregation::Second => "SECOND",
245 BarAggregation::Minute => "MINUTE",
246 BarAggregation::Hour => "HOUR",
247 BarAggregation::Day => "DAY",
248 BarAggregation::Week => "WEEK",
249 BarAggregation::Month => "MONTH",
250 BarAggregation::Year => "YEAR",
251 BarAggregation::Renko => "RENKO",
252 };
253 <&str as sqlx::Encode<sqlx::Postgres>>::encode(bar_aggregation_str, buf)
254 }
255}
256
257impl<'r> sqlx::Decode<'r, sqlx::Postgres> for BarAggregationPg {
258 fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
259 let bar_aggregation_str: &str = <&str as Decode<sqlx::Postgres>>::decode(value)?;
260 let bar_aggregation = BarAggregation::from_str(bar_aggregation_str).map_err(|_| {
261 sqlx::Error::Decode(format!("Invalid bar aggregation: {bar_aggregation_str}").into())
262 })?;
263 Ok(Self(bar_aggregation))
264 }
265}
266
267impl sqlx::Type<sqlx::Postgres> for BarAggregationPg {
268 fn type_info() -> sqlx::postgres::PgTypeInfo {
269 PgTypeInfo::with_name("bar_aggregation")
270 }
271
272 fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
273 *ty == Self::type_info() || <&str as Type<sqlx::Postgres>>::compatible(ty)
274 }
275}
276
277impl sqlx::Encode<'_, sqlx::Postgres> for PriceTypePg {
278 fn encode_by_ref(
279 &self,
280 buf: &mut <Postgres as Database>::ArgumentBuffer,
281 ) -> Result<IsNull, BoxDynError> {
282 let price_type_str = match self.0 {
283 PriceType::Bid => "BID",
284 PriceType::Ask => "ASK",
285 PriceType::Mid => "MID",
286 PriceType::Last => "LAST",
287 PriceType::Mark => "MARK",
288 };
289 <&str as sqlx::Encode<sqlx::Postgres>>::encode(price_type_str, buf)
290 }
291}
292
293impl<'r> sqlx::Decode<'r, sqlx::Postgres> for PriceTypePg {
294 fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
295 let price_type_str: &str = <&str as Decode<sqlx::Postgres>>::decode(value)?;
296 let price_type = PriceType::from_str(price_type_str).map_err(|_| {
297 sqlx::Error::Decode(format!("Invalid price type: {price_type_str}").into())
298 })?;
299 Ok(Self(price_type))
300 }
301}
302
303impl sqlx::Type<sqlx::Postgres> for PriceTypePg {
304 fn type_info() -> sqlx::postgres::PgTypeInfo {
305 PgTypeInfo::with_name("price_type")
306 }
307
308 fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
309 *ty == Self::type_info() || <&str as Type<sqlx::Postgres>>::compatible(ty)
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use nautilus_model::enums::{AccountType, BookAction, InstrumentClass, OrderStatus};
316 use regex::Regex;
317 use rstest::rstest;
318 use strum::IntoEnumIterator;
319
320 use super::*;
321
322 fn types_sql() -> String {
325 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../schema/sql/types.sql");
326 std::fs::read_to_string(path).expect("failed to read types.sql")
327 }
328
329 fn sql_enum_labels(type_name: &str) -> Vec<String> {
330 let sql = types_sql();
331 let declaration = Regex::new(&format!(
332 r"(?s)CREATE\s+TYPE\s+{type_name}\s+AS\s+ENUM\s*\((.*?)\);"
333 ))
334 .expect("invalid declaration pattern")
335 .captures(&sql)
336 .unwrap_or_else(|| panic!("no CREATE TYPE found for {type_name}"))[1]
337 .to_string();
338
339 Regex::new("'([A-Z_0-9]+)'")
340 .expect("invalid label pattern")
341 .captures_iter(&declaration)
342 .map(|label| label[1].to_string())
343 .collect()
344 }
345
346 fn rust_enum_labels<T: IntoEnumIterator + AsRef<str>>() -> Vec<String> {
347 T::iter().map(|value| value.as_ref().to_string()).collect()
348 }
349
350 fn guarded_sql_enum_types() -> Vec<(&'static str, Vec<String>)> {
353 vec![
354 ("ACCOUNT_TYPE", rust_enum_labels::<AccountType>()),
355 (
356 "AGGREGATION_SOURCE",
357 rust_enum_labels::<AggregationSource>(),
358 ),
359 ("AGGRESSOR_SIDE", rust_enum_labels::<AggressorSide>()),
360 ("ASSET_CLASS", rust_enum_labels::<AssetClass>()),
361 ("BAR_AGGREGATION", rust_enum_labels::<BarAggregation>()),
362 ("BOOK_ACTION", rust_enum_labels::<BookAction>()),
363 ("CURRENCY_TYPE", rust_enum_labels::<CurrencyType>()),
364 ("INSTRUMENT_CLASS", rust_enum_labels::<InstrumentClass>()),
365 ("ORDER_STATUS", rust_enum_labels::<OrderStatus>()),
366 ("PRICE_TYPE", rust_enum_labels::<PriceType>()),
367 (
368 "TRAILING_OFFSET_TYPE",
369 std::iter::once("NO_TRAILING_OFFSET".to_string())
370 .chain(rust_enum_labels::<TrailingOffsetType>())
371 .collect(),
372 ),
373 ]
374 }
375
376 #[rstest]
377 fn sql_enum_type_matches_rust_enum() {
378 for (type_name, expected) in guarded_sql_enum_types() {
379 assert_eq!(sql_enum_labels(type_name), expected, "{type_name}");
380 }
381 }
382
383 #[rstest]
384 fn every_declared_sql_enum_type_is_guarded() {
385 let mut declared: Vec<String> = Regex::new(r"CREATE\s+TYPE\s+(\w+)\s+AS\s+ENUM")
386 .expect("invalid type name pattern")
387 .captures_iter(&types_sql())
388 .map(|name| name[1].to_string())
389 .collect();
390 let mut guarded: Vec<String> = guarded_sql_enum_types()
391 .into_iter()
392 .map(|(type_name, _)| type_name.to_string())
393 .collect();
394 declared.sort();
395 guarded.sort();
396
397 assert_eq!(declared, guarded);
398 }
399
400 #[rstest]
401 #[case(AggressorSide::NoAggressor, "NO_AGGRESSOR")]
402 #[case(AggressorSide::Buy, "BUY")]
403 #[case(AggressorSide::Sell, "SELL")]
404 fn aggressor_side_pg_encodes_postgres_labels(
405 #[case] value: AggressorSide,
406 #[case] expected: &str,
407 ) {
408 let mut buf = sqlx::postgres::PgArgumentBuffer::default();
409 let _ = sqlx::Encode::<sqlx::Postgres>::encode(AggressorSidePg(value), &mut buf);
410 assert_eq!(&buf[..], expected.as_bytes());
411 }
412
413 #[rstest]
414 #[case(BarAggregation::Millisecond, "MILLISECOND")]
415 #[case(BarAggregation::Second, "SECOND")]
416 #[case(BarAggregation::Month, "MONTH")]
417 #[case(BarAggregation::Year, "YEAR")]
418 #[case(BarAggregation::Renko, "RENKO")]
419 fn bar_aggregation_pg_encodes_postgres_labels(
420 #[case] value: BarAggregation,
421 #[case] expected: &str,
422 ) {
423 let mut buf = sqlx::postgres::PgArgumentBuffer::default();
424 let _ = sqlx::Encode::<sqlx::Postgres>::encode(BarAggregationPg(value), &mut buf);
425 assert_eq!(&buf[..], expected.as_bytes());
426 assert_eq!(BarAggregation::from_str(expected), Ok(value));
427 }
428
429 #[rstest]
430 #[case(PriceType::Bid, "BID")]
431 #[case(PriceType::Ask, "ASK")]
432 #[case(PriceType::Mid, "MID")]
433 #[case(PriceType::Last, "LAST")]
434 #[case(PriceType::Mark, "MARK")]
435 fn price_type_pg_encodes_postgres_labels(#[case] value: PriceType, #[case] expected: &str) {
436 let mut buf = sqlx::postgres::PgArgumentBuffer::default();
437 let _ = sqlx::Encode::<sqlx::Postgres>::encode(PriceTypePg(value), &mut buf);
438 assert_eq!(&buf[..], expected.as_bytes());
439 assert_eq!(PriceType::from_str(expected), Ok(value));
440 }
441}