1use std::path::PathBuf;
6
7use diesel::{QueryableByName, connection::SimpleConnection, sql_types::BigInt};
8use iota_json_rpc_types::IotaTransactionBlockResponse;
9use iota_metrics::init_metrics;
10use tokio::task::JoinHandle;
11use tokio_util::sync::CancellationToken;
12use url::Url;
13
14use crate::{
15 IndexerMetrics,
16 config::{
17 DEFAULT_PRUNING_BATCH_SIZE, IngestionConfig, IotaNamesOptions, PruningOptions,
18 RetentionConfig,
19 },
20 db::{ConnectionPool, ConnectionPoolConfig, PoolConnection, new_connection_pool},
21 errors::IndexerError,
22 indexer::Indexer,
23 store::{PgIndexerAnalyticalStore, PgIndexerStore},
24};
25
26const TEST_PRUNING_DELAY_MS: u64 = 1000; pub type DBInitHook = Box<dyn FnOnce(&PgIndexerStore) + Send>;
66
67pub enum IndexerTypeConfig {
68 Reader {
69 reader_mode_rpc_url: String,
70 },
71 Writer {
72 retention_config: Option<RetentionConfig>,
73 pruning_delay_ms: u64,
74 pruning_batch_size: u64,
75 },
76 AnalyticalWorker,
77}
78
79impl IndexerTypeConfig {
80 pub fn reader_mode(reader_mode_rpc_url: String) -> Self {
81 Self::Reader {
82 reader_mode_rpc_url,
83 }
84 }
85
86 pub fn writer_mode(pruning_options: Option<PruningOptions>) -> Self {
87 let opts = pruning_options.unwrap_or_default();
88 Self::Writer {
89 retention_config: opts
90 .load_from_file()
91 .expect("failed to load the indexer retention configuration"),
92 pruning_delay_ms: TEST_PRUNING_DELAY_MS,
93 pruning_batch_size: opts.pruning_batch_size,
94 }
95 }
96
97 pub fn writer_mode_with_retention(retention_config: Option<RetentionConfig>) -> Self {
100 Self::Writer {
101 retention_config,
102 pruning_delay_ms: TEST_PRUNING_DELAY_MS,
103 pruning_batch_size: DEFAULT_PRUNING_BATCH_SIZE,
104 }
105 }
106}
107
108pub async fn start_test_indexer(
109 db_url: String,
110 reset_db: bool,
111 db_init_hook: Option<DBInitHook>,
112 rpc_url: String,
113 reader_writer_config: IndexerTypeConfig,
114 data_ingestion_path: Option<PathBuf>,
115) -> (
116 PgIndexerStore,
117 JoinHandle<Result<(), IndexerError>>,
118 CancellationToken,
119) {
120 let token = CancellationToken::new();
121 let (store, handle) = start_test_indexer_impl(
122 db_url,
123 reset_db,
124 db_init_hook,
125 rpc_url,
126 reader_writer_config,
127 data_ingestion_path,
128 token.clone(),
129 )
130 .await;
131 (store, handle, token)
132}
133
134pub async fn start_test_indexer_impl(
144 db_url: String,
145 reset_db: bool,
146 db_init_hook: Option<DBInitHook>,
147 rpc_url: String,
148 reader_writer_config: IndexerTypeConfig,
149 data_ingestion_path: Option<PathBuf>,
150 cancel: CancellationToken,
151) -> (PgIndexerStore, JoinHandle<Result<(), IndexerError>>) {
152 let store = create_pg_store(&db_url, reset_db);
153 if reset_db {
154 crate::db::reset_database(&mut store.blocking_cp().get().unwrap()).unwrap();
155 }
156 if let Some(db_init_hook) = db_init_hook {
157 db_init_hook(&store);
158 }
159
160 let registry = prometheus_filtered::Registry::default();
161 init_metrics(®istry);
162 let indexer_metrics = IndexerMetrics::new(®istry);
163
164 let handle = match reader_writer_config {
165 IndexerTypeConfig::Reader {
166 reader_mode_rpc_url,
167 } => {
168 let config = crate::config::JsonRpcConfig {
169 iota_names_options: IotaNamesOptions::default(),
170 historic_fallback_options: Default::default(),
171 rpc_address: reader_mode_rpc_url.parse().unwrap(),
172 rpc_client_url: rpc_url,
173 };
174 let pool = store.blocking_cp();
175 let store_clone = store.clone();
176 tokio::spawn(async move {
177 Indexer::start_reader(
178 &config,
179 store_clone,
180 ®istry,
181 pool,
182 indexer_metrics,
183 CancellationToken::new(),
184 )
185 .await
186 })
187 }
188 IndexerTypeConfig::Writer {
189 retention_config,
190 pruning_delay_ms,
191 pruning_batch_size,
192 } => {
193 let fullnode_rpc_url = rpc_url.parse::<Url>().unwrap();
194 let store_clone = store.clone();
195 let mut ingestion_config = IngestionConfig::default();
196 ingestion_config.sources.remote_store_url =
197 data_ingestion_path.is_none().then_some(fullnode_rpc_url);
198 ingestion_config.sources.data_ingestion_path = data_ingestion_path;
199
200 tokio::spawn(async move {
201 Indexer::start_writer_with_config(
202 &ingestion_config,
203 store_clone,
204 indexer_metrics,
205 retention_config,
206 pruning_delay_ms,
207 pruning_batch_size,
208 cancel,
209 )
210 .await
211 })
212 }
213 IndexerTypeConfig::AnalyticalWorker => {
214 let store = PgIndexerAnalyticalStore::new(store.blocking_cp());
215
216 tokio::spawn(
217 async move { Indexer::start_analytical_worker(store, indexer_metrics).await },
218 )
219 }
220 };
221
222 (store, handle)
223}
224
225pub struct TestDatabase {
227 pub url: String,
228 db_name: String,
229 connection: PoolConnection,
230 pool_config: ConnectionPoolConfig,
231}
232
233impl TestDatabase {
234 pub fn new(db_url: String) -> Self {
235 let pool_config = ConnectionPoolConfig {
238 pool_size: 5,
239 ..Default::default()
240 };
241
242 let db_name = db_url.split('/').next_back().unwrap().into();
243 let (default_url, _) = replace_db_name(&db_url, "postgres");
244 let blocking_pool = new_connection_pool(&default_url, &pool_config).unwrap();
245 let connection = blocking_pool.get().unwrap();
246 Self {
247 url: db_url,
248 db_name,
249 connection,
250 pool_config,
251 }
252 }
253
254 pub fn drop_if_exists(&mut self) {
256 self.connection
257 .batch_execute(&format!("DROP DATABASE IF EXISTS {}", self.db_name))
258 .unwrap();
259 }
260
261 pub fn create(&mut self) {
263 self.connection
264 .batch_execute(&format!("CREATE DATABASE {}", self.db_name))
265 .unwrap();
266 }
267
268 pub fn recreate(&mut self) {
270 self.drop_if_exists();
271 self.create();
272 }
273
274 pub fn to_connection_pool(&self) -> ConnectionPool {
276 new_connection_pool(&self.url, &self.pool_config).unwrap()
277 }
278
279 pub fn reset_db(&mut self) {
280 crate::db::reset_database(&mut self.to_connection_pool().get().unwrap()).unwrap();
281 }
282}
283
284pub fn create_pg_store(db_url: &str, reset_database: bool) -> PgIndexerStore {
285 let registry = prometheus_filtered::Registry::default();
286 init_metrics(®istry);
287 let indexer_metrics = IndexerMetrics::new(®istry);
288
289 let mut test_db = TestDatabase::new(db_url.to_string());
290 if reset_database {
291 test_db.recreate();
292 }
293
294 PgIndexerStore::new(test_db.to_connection_pool(), indexer_metrics)
295}
296
297fn replace_db_name(db_url: &str, new_db_name: &str) -> (String, String) {
298 let pos = db_url.rfind('/').expect("unable to find / in db_url");
299 let old_db_name = &db_url[pos + 1..];
300
301 (
302 format!("{}/{}", &db_url[..pos], new_db_name),
303 old_db_name.to_string(),
304 )
305}
306
307pub async fn force_delete_database(db_url: String) {
308 let (default_db_url, db_name) = replace_db_name(&db_url, "postgres");
313 let mut pool_config = ConnectionPoolConfig::default();
314 pool_config.set_pool_size(1);
315
316 let blocking_pool = new_connection_pool(&default_db_url, &pool_config).unwrap();
317 blocking_pool
318 .get()
319 .unwrap()
320 .batch_execute(&format!("DROP DATABASE IF EXISTS {db_name} WITH (FORCE)"))
321 .unwrap();
322}
323
324#[derive(Clone)]
325pub struct IotaTransactionBlockResponseBuilder<'a> {
326 response: IotaTransactionBlockResponse,
327 full_response: &'a IotaTransactionBlockResponse,
328}
329
330impl<'a> IotaTransactionBlockResponseBuilder<'a> {
331 pub fn new(full_response: &'a IotaTransactionBlockResponse) -> Self {
332 Self {
333 response: IotaTransactionBlockResponse::default(),
334 full_response,
335 }
336 }
337
338 pub fn with_input(mut self) -> Self {
339 self.response = IotaTransactionBlockResponse {
340 transaction: self.full_response.transaction.clone(),
341 ..self.response
342 };
343 self
344 }
345
346 pub fn with_raw_input(mut self) -> Self {
347 self.response = IotaTransactionBlockResponse {
348 raw_transaction: self.full_response.raw_transaction.clone(),
349 ..self.response
350 };
351 self
352 }
353
354 pub fn with_effects(mut self) -> Self {
355 self.response = IotaTransactionBlockResponse {
356 effects: self.full_response.effects.clone(),
357 ..self.response
358 };
359 self
360 }
361
362 pub fn with_events(mut self) -> Self {
363 self.response = IotaTransactionBlockResponse {
364 events: self.full_response.events.clone(),
365 ..self.response
366 };
367 self
368 }
369
370 pub fn with_balance_changes(mut self) -> Self {
371 self.response = IotaTransactionBlockResponse {
372 balance_changes: self.full_response.balance_changes.clone(),
373 ..self.response
374 };
375 self
376 }
377
378 pub fn with_object_changes(mut self) -> Self {
379 self.response = IotaTransactionBlockResponse {
380 object_changes: self.full_response.object_changes.clone(),
381 ..self.response
382 };
383 self
384 }
385
386 pub fn with_input_and_changes(mut self) -> Self {
387 self.response = IotaTransactionBlockResponse {
388 transaction: self.full_response.transaction.clone(),
389 balance_changes: self.full_response.balance_changes.clone(),
390 object_changes: self.full_response.object_changes.clone(),
391 ..self.response
392 };
393 self
394 }
395
396 pub fn build(self) -> IotaTransactionBlockResponse {
397 IotaTransactionBlockResponse {
398 transaction: self.response.transaction,
399 raw_transaction: self.response.raw_transaction,
400 effects: self.response.effects,
401 events: self.response.events,
402 balance_changes: self.response.balance_changes,
403 object_changes: self.response.object_changes,
404 ..self.full_response.clone()
406 }
407 }
408}
409
410pub fn db_url(db_name: &str) -> String {
414 format!("postgres://postgres:postgrespw@localhost:5432/{db_name}")
415}
416
417#[derive(QueryableByName, Debug)]
419pub struct RowCount {
420 #[diesel(sql_type = BigInt)]
421 pub cnt: i64,
422}