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, DbUrl, 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 store
158 .execute_in_blocking_worker(move |this| {
159 db_init_hook(&this);
160 Ok(())
161 })
162 .await
163 .expect("failed to run the DB init hook");
164 }
165
166 let registry = prometheus_filtered::Registry::default();
167 init_metrics(®istry);
168 let indexer_metrics = IndexerMetrics::new(®istry);
169
170 let handle = match reader_writer_config {
171 IndexerTypeConfig::Reader {
172 reader_mode_rpc_url,
173 } => {
174 let config = crate::config::JsonRpcConfig {
175 iota_names_options: IotaNamesOptions::default(),
176 historic_fallback_options: Default::default(),
177 rpc_address: reader_mode_rpc_url.parse().unwrap(),
178 rpc_client_url: rpc_url,
179 };
180 let pool = store.blocking_cp();
181 let store_clone = store.clone();
182 tokio::spawn(async move {
183 Indexer::start_reader(
184 &config,
185 store_clone,
186 ®istry,
187 pool,
188 indexer_metrics,
189 CancellationToken::new(),
190 )
191 .await
192 })
193 }
194 IndexerTypeConfig::Writer {
195 retention_config,
196 pruning_delay_ms,
197 pruning_batch_size,
198 } => {
199 let fullnode_rpc_url = rpc_url.parse::<Url>().unwrap();
200 let store_clone = store.clone();
201 let mut ingestion_config = IngestionConfig::default();
202 ingestion_config.sources.remote_store_url =
203 data_ingestion_path.is_none().then_some(fullnode_rpc_url);
204 ingestion_config.sources.data_ingestion_path = data_ingestion_path;
205
206 tokio::spawn(async move {
207 Indexer::start_writer_with_config(
208 &ingestion_config,
209 store_clone,
210 indexer_metrics,
211 retention_config,
212 pruning_delay_ms,
213 pruning_batch_size,
214 cancel,
215 )
216 .await
217 })
218 }
219 IndexerTypeConfig::AnalyticalWorker => {
220 let store = PgIndexerAnalyticalStore::new(store.blocking_cp());
221
222 tokio::spawn(
223 async move { Indexer::start_analytical_worker(store, indexer_metrics).await },
224 )
225 }
226 };
227
228 (store, handle)
229}
230
231pub struct TestDatabase {
233 pub url: String,
234 db_name: String,
235 connection: PoolConnection,
236 pool_config: ConnectionPoolConfig,
237}
238
239impl TestDatabase {
240 pub fn new(db_url: String) -> Self {
241 let pool_config = ConnectionPoolConfig {
244 pool_size: 5,
245 ..Default::default()
246 };
247
248 let db_name = db_url.split('/').next_back().unwrap().into();
249 let (default_url, _) = replace_db_name(&db_url, "postgres");
250 let blocking_pool = new_connection_pool(&DbUrl::from(default_url), &pool_config).unwrap();
251 let connection = blocking_pool.get().unwrap();
252 Self {
253 url: db_url,
254 db_name,
255 connection,
256 pool_config,
257 }
258 }
259
260 pub fn drop_if_exists(&mut self) {
262 self.connection
263 .batch_execute(&format!("DROP DATABASE IF EXISTS {}", self.db_name))
264 .unwrap();
265 }
266
267 pub fn create(&mut self) {
269 self.connection
270 .batch_execute(&format!("CREATE DATABASE {}", self.db_name))
271 .unwrap();
272 }
273
274 pub fn recreate(&mut self) {
276 self.drop_if_exists();
277 self.create();
278 }
279
280 pub fn to_connection_pool(&self) -> ConnectionPool {
282 new_connection_pool(&DbUrl::from(self.url.as_str()), &self.pool_config).unwrap()
283 }
284
285 pub fn reset_db(&mut self) {
286 crate::db::reset_database(&mut self.to_connection_pool().get().unwrap()).unwrap();
287 }
288}
289
290pub fn create_pg_store(db_url: &str, reset_database: bool) -> PgIndexerStore {
291 let registry = prometheus_filtered::Registry::default();
292 init_metrics(®istry);
293 let indexer_metrics = IndexerMetrics::new(®istry);
294
295 let mut test_db = TestDatabase::new(db_url.to_string());
296 if reset_database {
297 test_db.recreate();
298 }
299
300 PgIndexerStore::new(test_db.to_connection_pool(), indexer_metrics)
301}
302
303fn replace_db_name(db_url: &str, new_db_name: &str) -> (String, String) {
304 let pos = db_url.rfind('/').expect("unable to find / in db_url");
305 let old_db_name = &db_url[pos + 1..];
306
307 (
308 format!("{}/{}", &db_url[..pos], new_db_name),
309 old_db_name.to_string(),
310 )
311}
312
313pub async fn force_delete_database(db_url: String) {
314 let (default_db_url, db_name) = replace_db_name(&db_url, "postgres");
319 let mut pool_config = ConnectionPoolConfig::default();
320 pool_config.set_pool_size(1);
321
322 let blocking_pool = new_connection_pool(&DbUrl::from(default_db_url), &pool_config).unwrap();
323 blocking_pool
324 .get()
325 .unwrap()
326 .batch_execute(&format!("DROP DATABASE IF EXISTS {db_name} WITH (FORCE)"))
327 .unwrap();
328}
329
330#[derive(Clone)]
331pub struct IotaTransactionBlockResponseBuilder<'a> {
332 response: IotaTransactionBlockResponse,
333 full_response: &'a IotaTransactionBlockResponse,
334}
335
336impl<'a> IotaTransactionBlockResponseBuilder<'a> {
337 pub fn new(full_response: &'a IotaTransactionBlockResponse) -> Self {
338 Self {
339 response: IotaTransactionBlockResponse::default(),
340 full_response,
341 }
342 }
343
344 pub fn with_input(mut self) -> Self {
345 self.response = IotaTransactionBlockResponse {
346 transaction: self.full_response.transaction.clone(),
347 ..self.response
348 };
349 self
350 }
351
352 pub fn with_raw_input(mut self) -> Self {
353 self.response = IotaTransactionBlockResponse {
354 raw_transaction: self.full_response.raw_transaction.clone(),
355 ..self.response
356 };
357 self
358 }
359
360 pub fn with_effects(mut self) -> Self {
361 self.response = IotaTransactionBlockResponse {
362 effects: self.full_response.effects.clone(),
363 ..self.response
364 };
365 self
366 }
367
368 pub fn with_events(mut self) -> Self {
369 self.response = IotaTransactionBlockResponse {
370 events: self.full_response.events.clone(),
371 ..self.response
372 };
373 self
374 }
375
376 pub fn with_balance_changes(mut self) -> Self {
377 self.response = IotaTransactionBlockResponse {
378 balance_changes: self.full_response.balance_changes.clone(),
379 ..self.response
380 };
381 self
382 }
383
384 pub fn with_object_changes(mut self) -> Self {
385 self.response = IotaTransactionBlockResponse {
386 object_changes: self.full_response.object_changes.clone(),
387 ..self.response
388 };
389 self
390 }
391
392 pub fn with_input_and_changes(mut self) -> Self {
393 self.response = IotaTransactionBlockResponse {
394 transaction: self.full_response.transaction.clone(),
395 balance_changes: self.full_response.balance_changes.clone(),
396 object_changes: self.full_response.object_changes.clone(),
397 ..self.response
398 };
399 self
400 }
401
402 pub fn build(self) -> IotaTransactionBlockResponse {
403 IotaTransactionBlockResponse {
404 transaction: self.response.transaction,
405 raw_transaction: self.response.raw_transaction,
406 effects: self.response.effects,
407 events: self.response.events,
408 balance_changes: self.response.balance_changes,
409 object_changes: self.response.object_changes,
410 ..self.full_response.clone()
412 }
413 }
414}
415
416pub fn db_url(db_name: &str) -> String {
420 format!("postgres://postgres:postgrespw@localhost:5432/{db_name}")
421}
422
423#[derive(QueryableByName, Debug)]
425pub struct RowCount {
426 #[diesel(sql_type = BigInt)]
427 pub cnt: i64,
428}