db_sync_sqlx/
lib.rs

1//! helpers and primitive types for querying Cardano [Db-Sync] data using [sqlx]
2//!
3//! # About
4//!
5//! This crate is meant to provide help when writing queries for data produced by [Db-Sync],
6//! a Cardano blockchain indexer. Db-Sync keeps the indexed data in a Postgre database that
7//! can be queried using SQL. This crate defines primitive types that correspond to column
8//! types used in the [Db-Sync schema], along with some other helpers and casts to types
9//! defined in [sidechain_domain].
10//!
11//! [sqlx]: https://github.com/launchbadge/sqlx
12//! [Db-Sync]: https://github.com/IntersectMBO/cardano-db-sync
13//! [Db-Sync schema]: https://github.com/IntersectMBO/cardano-db-sync/blob/master/doc/schema.md
14
15use num_traits::ToPrimitive;
16use sidechain_domain::*;
17use sqlx::database::Database;
18use sqlx::encode::IsNull;
19use sqlx::error::BoxDynError;
20use sqlx::postgres::PgTypeInfo;
21use sqlx::*;
22
23/// Macro to handle numeric types that are non-negative but are stored by Db-Sync using
24/// signed SQL types.
25///
26/// This macro generates an unsigned numeric domain type and sqlx trait implementation for
27/// decoding it from signed data coming from Db-Sync database. It expects that values will
28/// always have 0 as the most significant bit.
29///
30/// For example because [TxIndex] is in range of \[0, 2^15-1\], it will be [u16] in domain,
31/// but it requires encoding and decoding as i16.
32///
33/// See `txindex`, `word31` `and` word63 types in Db-Sync schema definition.
34#[macro_export]
35macro_rules! sqlx_implementations_for_wrapper {
36	($WRAPPED:ty, $DBTYPE:expr, $NAME:ty, $DOMAIN:ty) => {
37		impl sqlx::Type<Postgres> for $NAME {
38			fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
39				PgTypeInfo::with_name($DBTYPE)
40			}
41		}
42
43		impl<'r> Decode<'r, Postgres> for $NAME
44		where
45			$WRAPPED: Decode<'r, Postgres>,
46		{
47			fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
48				let decoded: $WRAPPED = <$WRAPPED as Decode<Postgres>>::decode(value)?;
49				Ok(Self(decoded.try_into()?))
50			}
51		}
52
53		#[cfg(test)]
54		impl From<$WRAPPED> for $NAME {
55			fn from(value: $WRAPPED) -> Self {
56				Self(value.try_into().expect("value from domain fits in type db type"))
57			}
58		}
59
60		impl<'q> Encode<'q, Postgres> for $NAME {
61			fn encode_by_ref(
62				&self,
63				buf: &mut <Postgres as Database>::ArgumentBuffer<'q>,
64			) -> std::result::Result<IsNull, BoxDynError> {
65				buf.extend(&self.0.to_be_bytes());
66				Ok(IsNull::No)
67			}
68		}
69
70		impl From<$NAME> for $DOMAIN {
71			fn from(value: $NAME) -> Self {
72				Self(value.0)
73			}
74		}
75
76		impl From<$DOMAIN> for $NAME {
77			fn from(value: $DOMAIN) -> Self {
78				Self(value.0)
79			}
80		}
81	};
82}
83
84/// Cardano block number
85#[derive(Debug, Copy, Ord, PartialOrd, Clone, PartialEq, Eq, Default)]
86pub struct BlockNumber(pub u32);
87sqlx_implementations_for_wrapper!(i32, "INT4", BlockNumber, McBlockNumber);
88
89impl BlockNumber {
90	pub fn saturating_add<Rhs: Into<u32>>(self, rhs: Rhs) -> Self {
91		Self(self.0.saturating_add(rhs.into()))
92	}
93
94	pub fn saturating_sub<Rhs: Into<u32>>(self, rhs: Rhs) -> Self {
95		Self(self.0.saturating_sub(rhs.into()))
96	}
97}
98
99/// Cardano epoch number
100#[derive(Debug, Copy, Clone, PartialEq)]
101pub struct EpochNumber(pub u32);
102sqlx_implementations_for_wrapper!(i32, "INT4", EpochNumber, McEpochNumber);
103
104/// Cardano slot number
105#[derive(Debug, Copy, Clone, PartialEq)]
106pub struct SlotNumber(pub u64);
107sqlx_implementations_for_wrapper!(i64, "INT8", SlotNumber, McSlotNumber);
108
109/// Index of a Cardano transaction output
110///
111/// This type corresponds to the following SQL type defined by Db-Sync.
112/// ```sql
113/// CREATE DOMAIN txindex AS smallint CONSTRAINT txindex_check CHECK ((VALUE >= 0));
114/// ```
115#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
116pub struct TxIndex(pub u16);
117sqlx_implementations_for_wrapper!(i16, "INT2", TxIndex, UtxoIndex);
118
119/// Index of a Cardano transaction with its block
120#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
121pub struct TxIndexInBlock(pub u32);
122sqlx_implementations_for_wrapper!(i32, "INT4", TxIndexInBlock, McTxIndexInBlock);
123
124#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
125pub struct TxHash(pub [u8; 32]);
126
127impl sqlx::Type<Postgres> for TxHash {
128	fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
129		PgTypeInfo::with_name("bytea")
130	}
131}
132
133impl<'r> Decode<'r, Postgres> for TxHash {
134	fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
135		let decoded = <[u8; 32]>::decode(value)?;
136		Ok(Self(decoded))
137	}
138}
139
140impl<'r> Encode<'r, Postgres> for TxHash {
141	fn encode_by_ref(
142		&self,
143		buf: &mut <Postgres as Database>::ArgumentBuffer<'r>,
144	) -> std::result::Result<IsNull, BoxDynError> {
145		buf.extend(self.0.iter());
146		Ok(IsNull::No)
147	}
148}
149
150impl From<TxHash> for McTxHash {
151	fn from(hash: TxHash) -> Self {
152		Self(hash.0)
153	}
154}
155impl From<McTxHash> for TxHash {
156	fn from(hash: McTxHash) -> Self {
157		Self(hash.0)
158	}
159}
160
161/// Number of ADA expressed in Lovelace (1 million-th of ADA)
162///
163/// This type corresponds to the following SQL type defined by Db-Sync.
164/// ```sql
165/// CREATE DOMAIN int65type AS numeric (20, 0) CHECK (VALUE >= -18446744073709551615 AND VALUE <= 18446744073709551615);
166/// ```
167#[derive(Debug, Clone, PartialEq)]
168pub struct TxValue(pub i128);
169
170impl sqlx::Type<Postgres> for TxValue {
171	fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
172		PgTypeInfo::with_name("NUMERIC")
173	}
174}
175
176impl<'r> Decode<'r, Postgres> for TxValue {
177	fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
178		let decoded = <sqlx::types::BigDecimal as Decode<Postgres>>::decode(value)?;
179		let i = decoded.to_i128().ok_or("TxValue is always an integer".to_string())?;
180		Ok(Self(i))
181	}
182}
183
184/// Number of ADA delegated by a Cardano delegator to a single SPO, expressed in Lovelace (1 million-th of ADA)
185///
186/// This type corresponds to the following SQL type defined by Db-Sync.
187/// ```sql
188/// CREATE DOMAIN "lovelace" AS numeric(20,0) CONSTRAINT flyway_needs_this CHECK (VALUE >= 0::numeric AND VALUE <= '18446744073709551615'::numeric);
189/// ```
190#[derive(Debug, Clone, PartialEq)]
191pub struct StakeDelegation(pub u64);
192
193impl sqlx::Type<Postgres> for StakeDelegation {
194	fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
195		PgTypeInfo::with_name("NUMERIC")
196	}
197}
198
199impl<'r> Decode<'r, Postgres> for StakeDelegation {
200	fn decode(value: <Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
201		let decoded = <sqlx::types::BigDecimal as Decode<Postgres>>::decode(value)?;
202		let i = decoded.to_u64().ok_or("StakeDelegation is always a u64".to_string())?;
203		Ok(Self(i))
204	}
205}
206
207/// Cardano native asset name, typically UTF-8 encoding of a human-readable name
208#[derive(Debug, Clone, sqlx::FromRow, PartialEq)]
209pub struct AssetName(pub Vec<u8>);
210
211impl From<sidechain_domain::AssetName> for AssetName {
212	fn from(name: sidechain_domain::AssetName) -> Self {
213		Self(name.0.to_vec())
214	}
215}
216
217/// Cardano minting policy ID. This value is obtained by hashing the Plutus script of the policy.
218#[derive(Debug, Clone, sqlx::FromRow, PartialEq)]
219pub struct PolicyId(pub Vec<u8>);
220
221impl From<sidechain_domain::PolicyId> for PolicyId {
222	fn from(id: sidechain_domain::PolicyId) -> Self {
223		Self(id.0.to_vec())
224	}
225}
226
227/// Full identifier of a Cardano native asset
228#[derive(Debug, Clone, sqlx::FromRow, PartialEq)]
229pub struct Asset {
230	/// Minting policy ID of the asset
231	pub policy_id: PolicyId,
232	/// Asset name
233	pub asset_name: AssetName,
234}
235
236impl Asset {
237	/// Creates an Asset with empty asset_name.
238	pub fn new(policy_id: sidechain_domain::PolicyId) -> Self {
239		Self { policy_id: PolicyId(policy_id.0.to_vec()), asset_name: AssetName(vec![]) }
240	}
241}
242
243impl From<sidechain_domain::AssetId> for Asset {
244	fn from(asset: AssetId) -> Self {
245		Self { policy_id: asset.policy_id.into(), asset_name: asset.asset_name.into() }
246	}
247}
248
249/// Helper row type for querying just epoch number
250#[allow(deprecated)]
251mod epoch_number_row {
252	use super::*;
253	#[deprecated(
254		since = "1.7.0",
255		note = "Deprecated due to not being either a primitive type or a complete Db-Sync table row."
256	)]
257	#[derive(Debug, Copy, Clone, sqlx::FromRow, PartialEq)]
258	pub struct EpochNumberRow(pub EpochNumber);
259
260	#[allow(deprecated)]
261	impl From<EpochNumberRow> for EpochNumber {
262		fn from(r: EpochNumberRow) -> Self {
263			r.0
264		}
265	}
266}
267pub use epoch_number_row::*;
268
269/// Cardano address in human-readable form. Either Base58 for Byron addresses and Bech32 for Shelley.
270#[derive(Debug, Clone, sqlx::FromRow, PartialEq)]
271pub struct Address(pub String);
272
273impl From<MainchainAddress> for Address {
274	fn from(addr: MainchainAddress) -> Self {
275		Self(addr.to_string())
276	}
277}
278
279/// Cardano epoch nonce, ie. random 32 bytes generated by Cardano every epoch.
280///
281/// This value can be used as a tamper-proof randomness seed.
282#[derive(Debug, Clone, sqlx::FromRow, PartialEq)]
283pub struct MainchainEpochNonce(pub Vec<u8>);