Procházet zdrojové kódy

fix(fortuna): use query builder and add comprehensive indices

Connor Prussin před 5 měsíci
rodič
revize
6e9c68a6bb

+ 0 - 116
apps/fortuna/.sqlx/query-9ab85443388dbac329b8c3031c33aa4767439d43238eb95e8625e377af46ecc1.json

@@ -1,116 +0,0 @@
-{
-  "db_name": "SQLite",
-  "query": "\n              SELECT * FROM request\n              WHERE\n                created_at >= $1\n                AND created_at <= $2\n                AND (\n                  $3 IS NULL\n                  OR CONCAT('0x', request_tx_hash) = $3\n                  OR CONCAT('0x', reveal_tx_hash) = $3\n                  OR CONCAT('0x', sender) = $3\n                  OR sequence = $3\n                )\n                AND ($4 IS NULL OR network_id = $4)\n                AND ($5 IS NULL OR state = $5)\n              ORDER BY created_at DESC\n              LIMIT $6\n              OFFSET $7\n            ",
-  "describe": {
-    "columns": [
-      {
-        "name": "chain_id",
-        "ordinal": 0,
-        "type_info": "Text"
-      },
-      {
-        "name": "network_id",
-        "ordinal": 1,
-        "type_info": "Integer"
-      },
-      {
-        "name": "provider",
-        "ordinal": 2,
-        "type_info": "Text"
-      },
-      {
-        "name": "sequence",
-        "ordinal": 3,
-        "type_info": "Integer"
-      },
-      {
-        "name": "created_at",
-        "ordinal": 4,
-        "type_info": "Datetime"
-      },
-      {
-        "name": "last_updated_at",
-        "ordinal": 5,
-        "type_info": "Datetime"
-      },
-      {
-        "name": "state",
-        "ordinal": 6,
-        "type_info": "Text"
-      },
-      {
-        "name": "request_block_number",
-        "ordinal": 7,
-        "type_info": "Integer"
-      },
-      {
-        "name": "request_tx_hash",
-        "ordinal": 8,
-        "type_info": "Text"
-      },
-      {
-        "name": "user_random_number",
-        "ordinal": 9,
-        "type_info": "Text"
-      },
-      {
-        "name": "sender",
-        "ordinal": 10,
-        "type_info": "Text"
-      },
-      {
-        "name": "reveal_block_number",
-        "ordinal": 11,
-        "type_info": "Integer"
-      },
-      {
-        "name": "reveal_tx_hash",
-        "ordinal": 12,
-        "type_info": "Text"
-      },
-      {
-        "name": "provider_random_number",
-        "ordinal": 13,
-        "type_info": "Text"
-      },
-      {
-        "name": "info",
-        "ordinal": 14,
-        "type_info": "Text"
-      },
-      {
-        "name": "gas_used",
-        "ordinal": 15,
-        "type_info": "Text"
-      },
-      {
-        "name": "gas_limit",
-        "ordinal": 16,
-        "type_info": "Text"
-      }
-    ],
-    "parameters": {
-      "Right": 7
-    },
-    "nullable": [
-      false,
-      false,
-      false,
-      false,
-      false,
-      false,
-      false,
-      false,
-      false,
-      false,
-      false,
-      true,
-      true,
-      true,
-      true,
-      true,
-      false
-    ]
-  },
-  "hash": "9ab85443388dbac329b8c3031c33aa4767439d43238eb95e8625e377af46ecc1"
-}

+ 10 - 6
apps/fortuna/flake.nix

@@ -19,17 +19,20 @@
     flake-utils.lib.eachDefaultSystem
     (
       system: let
-        cli-overlay = nixpkgs.lib.composeExtensions mkCli.overlays.default (_: prev: {
+        cli-overlay = nixpkgs.lib.composeExtensions mkCli.overlays.default (_: prev: let
+          cargo = "cargo --color=always";
+        in {
           cli = prev.lib.mkCli "cli" {
             _noAll = true;
-            start = "cargo sqlx migrate run && cargo sqlx prepare && RUST_LOG=info cargo run run";
+            start = "${cargo} sqlx migrate run && ${cargo} sqlx prepare && RUST_LOG=info ${cargo} watch -x 'run -- run'";
             test = {
-              format = "cargo fmt --check";
-              lint = "cargo clippy";
+              format = "${cargo} fmt --check";
+              lint = "${cargo} sqlx migrate run && ${cargo} sqlx prepare && ${cargo} clippy --color always";
+              unit = "${cargo} sqlx migrate run && ${cargo} sqlx prepare && ${cargo} test -- --color always";
             };
             fix = {
-              format = "cargo fmt";
-              lint = "cargo clippy --fix";
+              format = "${cargo} fmt";
+              lint = "${cargo} sqlx migrate run && ${cargo} sqlx prepare && ${cargo} clippy -- --error-format human --color always --fix";
             };
           };
         });
@@ -51,6 +54,7 @@
             pkgs.sqlx-cli
             pkgs.foundry
             pkgs.sqlite
+            pkgs.cargo-watch
           ];
         };
       }

+ 22 - 0
apps/fortuna/migrations/20250605004757_add_indices_for_advanced_search.down.sql

@@ -0,0 +1,22 @@
+-- Add down migration script here
+
+DROP INDEX request__network_id__state__created_at;
+DROP INDEX request__network_id__created_at;
+DROP INDEX request__sender__network_id__state__created_at;
+DROP INDEX request__sender__network_id__created_at;
+DROP INDEX request__sender__state__created_at;
+DROP INDEX request__sender__created_at;
+DROP INDEX request__sequence__network_id__state__created_at;
+DROP INDEX request__sequence__network_id__created_at;
+DROP INDEX request__sequence__state__created_at;
+DROP INDEX request__sequence__created_at;
+DROP INDEX request__state__created_at;
+DROP INDEX request__created_at;
+
+
+CREATE INDEX idx_request_sequence ON request (sequence);
+CREATE INDEX idx_request_network_id_created_at ON request (network_id, created_at);
+CREATE INDEX idx_request_created_at ON request (created_at);
+CREATE INDEX idx_request_request_tx_hash ON request (request_tx_hash) WHERE request_tx_hash IS NOT NULL;
+CREATE INDEX idx_request_reveal_tx_hash ON request (reveal_tx_hash) WHERE reveal_tx_hash IS NOT NULL;
+CREATE INDEX idx_request_sender ON request (sender) WHERE sender IS NOT NULL;

+ 22 - 0
apps/fortuna/migrations/20250605004757_add_indices_for_advanced_search.up.sql

@@ -0,0 +1,22 @@
+-- Add up migration script here
+
+DROP INDEX idx_request_sequence;
+DROP INDEX idx_request_network_id_created_at;
+DROP INDEX idx_request_created_at;
+DROP INDEX idx_request_request_tx_hash;
+DROP INDEX idx_request_reveal_tx_hash;
+DROP INDEX idx_request_sender;
+
+
+CREATE INDEX request__network_id__state__created_at ON request(network_id, state, created_at);
+CREATE INDEX request__network_id__created_at ON request(network_id, created_at);
+CREATE INDEX request__sender__network_id__state__created_at ON request(sender, network_id, state, created_at);
+CREATE INDEX request__sender__network_id__created_at ON request(sender, network_id, created_at);
+CREATE INDEX request__sender__state__created_at ON request(sender, state, created_at);
+CREATE INDEX request__sender__created_at ON request(sender, created_at);
+CREATE INDEX request__sequence__network_id__state__created_at ON request(sequence, network_id, state, created_at);
+CREATE INDEX request__sequence__network_id__created_at ON request(sequence, network_id, created_at);
+CREATE INDEX request__sequence__state__created_at ON request(sequence, state, created_at);
+CREATE INDEX request__sequence__created_at ON request(sequence, created_at);
+CREATE INDEX request__state__created_at ON request(state, created_at);
+CREATE INDEX request__created_at ON request(created_at);

+ 3 - 1
apps/fortuna/src/api/explorer.rs

@@ -65,7 +65,9 @@ pub async fn explorer(
     }
     let mut query = state.history.query();
     if let Some(search) = query_params.query {
-        query = query.search(search);
+        query = query
+            .search(search)
+            .map_err(|_| RestError::InvalidQueryString)?;
     }
     if let Some(network_id) = query_params.network_id {
         query = query.network_id(network_id);

+ 139 - 41
apps/fortuna/src/history.rs

@@ -10,8 +10,8 @@ use {
     },
     serde::Serialize,
     serde_with::serde_as,
-    sqlx::{migrate, Pool, Sqlite, SqlitePool},
-    std::sync::Arc,
+    sqlx::{migrate, FromRow, Pool, QueryBuilder, Sqlite, SqlitePool},
+    std::{str::FromStr, sync::Arc},
     tokio::{spawn, sync::mpsc},
     utoipa::ToSchema,
 };
@@ -99,7 +99,7 @@ impl RequestStatus {
     }
 }
 
-#[derive(Clone, Debug, Serialize, ToSchema, PartialEq)]
+#[derive(Clone, Debug, Serialize, ToSchema, PartialEq, FromRow)]
 struct RequestRow {
     chain_id: String,
     network_id: i64,
@@ -345,7 +345,7 @@ impl History {
 #[derive(Debug, Clone)]
 pub struct RequestQueryBuilder<'a> {
     pool: &'a Pool<Sqlite>,
-    search: Option<String>,
+    search: Option<SearchField>,
     network_id: Option<i64>,
     state: Option<StateTag>,
     limit: i64,
@@ -374,9 +374,20 @@ impl<'a> RequestQueryBuilder<'a> {
         }
     }
 
-    pub fn search(mut self, search: String) -> Self {
-        self.search = Some(search);
-        self
+    pub fn search(mut self, search: String) -> Result<Self, RequestQueryBuilderError> {
+        if let Ok(tx_hash) = TxHash::from_str(&search) {
+            Ok(SearchField::TxHash(tx_hash))
+        } else if let Ok(sender) = Address::from_str(&search) {
+            Ok(SearchField::Sender(sender))
+        } else if let Ok(sequence_number) = u64::from_str(&search) {
+            Ok(SearchField::SequenceNumber(sequence_number as i64))
+        } else {
+            Err(RequestQueryBuilderError::InvalidSearch)
+        }
+        .map(|search_field| {
+            self.search = Some(search_field);
+            self
+        })
     }
 
     pub fn network_id(mut self, network_id: NetworkId) -> Self {
@@ -416,41 +427,58 @@ impl<'a> RequestQueryBuilder<'a> {
     }
 
     pub async fn execute(&self) -> Result<Vec<RequestStatus>> {
-        let rows = sqlx::query_as!(
-            RequestRow,
-            r"
-              SELECT * FROM request
-              WHERE
-                created_at >= $1
-                AND created_at <= $2
-                AND (
-                  $3 IS NULL
-                  OR CONCAT('0x', request_tx_hash) = $3
-                  OR CONCAT('0x', reveal_tx_hash) = $3
-                  OR CONCAT('0x', sender) = $3
-                  OR sequence = $3
-                )
-                AND ($4 IS NULL OR network_id = $4)
-                AND ($5 IS NULL OR state = $5)
-              ORDER BY created_at DESC
-              LIMIT $6
-              OFFSET $7
-            ",
-            self.min_timestamp,
-            self.max_timestamp,
-            self.search,
-            self.network_id,
-            self.state,
-            self.limit,
-            self.offset
-        )
-        .fetch_all(self.pool)
-        .await
-        .map_err(|e| {
+        let mut query_builder =
+            QueryBuilder::new("SELECT * FROM request WHERE created_at BETWEEN ");
+        query_builder.push_bind(self.min_timestamp);
+        query_builder.push(" AND ");
+        query_builder.push_bind(self.max_timestamp);
+
+        match &self.search {
+            Some(SearchField::TxHash(tx_hash)) => {
+                let tx_hash: String = tx_hash.encode_hex();
+                query_builder.push(" AND (request_tx_hash = ");
+                query_builder.push_bind(tx_hash.clone());
+                query_builder.push(" OR reveal_tx_hash = ");
+                query_builder.push_bind(tx_hash);
+                query_builder.push(")");
+            }
+            Some(SearchField::Sender(sender)) => {
+                let sender: String = sender.encode_hex();
+                query_builder.push(" AND sender = ");
+                query_builder.push_bind(sender);
+            }
+            Some(SearchField::SequenceNumber(sequence_number)) => {
+                query_builder.push(" AND sequence = ");
+                query_builder.push_bind(sequence_number);
+            }
+            None => (),
+        }
+
+        if let Some(network_id) = &self.network_id {
+            query_builder.push(" AND network_id = ");
+            query_builder.push_bind(network_id);
+        }
+
+        if let Some(state) = &self.state {
+            query_builder.push(" AND state = ");
+            query_builder.push_bind(state);
+        }
+
+        query_builder.push(" ORDER BY created_at DESC LIMIT ");
+        query_builder.push_bind(self.limit);
+        query_builder.push(" OFFSET ");
+        query_builder.push_bind(self.offset);
+
+        let rows = query_builder
+            .build_query_as::<RequestRow>()
+            .fetch_all(self.pool)
+            .await;
+
+        if let Err(e) = &rows {
             tracing::error!("Failed to fetch request by time: {}", e);
-            e
-        })?;
-        Ok(rows.into_iter().filter_map(|row| row.into()).collect())
+        }
+
+        Ok(rows?.into_iter().filter_map(|row| row.into()).collect())
     }
 }
 
@@ -458,6 +486,14 @@ impl<'a> RequestQueryBuilder<'a> {
 pub enum RequestQueryBuilderError {
     LimitTooLarge,
     ZeroLimit,
+    InvalidSearch,
+}
+
+#[derive(Debug, Clone)]
+enum SearchField {
+    TxHash(TxHash),
+    Sender(Address),
+    SequenceNumber(i64),
 }
 
 #[cfg(test)]
@@ -506,6 +542,7 @@ mod test {
         let logs = history
             .query()
             .search(status.sequence.to_string())
+            .unwrap()
             .network_id(status.network_id)
             .execute()
             .await
@@ -515,6 +552,17 @@ mod test {
         let logs = history
             .query()
             .search(status.sequence.to_string())
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(status.sequence.to_string())
+            .unwrap()
+            .state(StateTag::Completed)
             .execute()
             .await
             .unwrap();
@@ -523,6 +571,26 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(status.request_tx_hash))
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(to_hex_string(status.request_tx_hash))
+            .unwrap()
+            .state(StateTag::Completed)
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(to_hex_string(reveal_tx_hash))
+            .unwrap()
             .execute()
             .await
             .unwrap();
@@ -531,6 +599,8 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(reveal_tx_hash))
+            .unwrap()
+            .state(StateTag::Completed)
             .execute()
             .await
             .unwrap();
@@ -539,6 +609,7 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(status.sender))
+            .unwrap()
             .network_id(status.network_id)
             .execute()
             .await
@@ -548,6 +619,17 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(status.sender))
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(to_hex_string(status.sender))
+            .unwrap()
+            .state(StateTag::Completed)
             .execute()
             .await
             .unwrap();
@@ -581,6 +663,7 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(reveal_tx_hash))
+            .unwrap()
             .execute()
             .await
             .unwrap();
@@ -600,6 +683,7 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(status.request_tx_hash))
+            .unwrap()
             .execute()
             .await
             .unwrap();
@@ -636,6 +720,7 @@ mod test {
         let logs = history
             .query()
             .search(status.sequence.to_string())
+            .unwrap()
             .network_id(123)
             .execute()
             .await
@@ -645,6 +730,7 @@ mod test {
         let logs = history
             .query()
             .search((status.sequence + 1).to_string())
+            .unwrap()
             .execute()
             .await
             .unwrap();
@@ -653,6 +739,7 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(TxHash::zero()))
+            .unwrap()
             .execute()
             .await
             .unwrap();
@@ -661,6 +748,7 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(Address::zero()))
+            .unwrap()
             .network_id(status.network_id)
             .execute()
             .await
@@ -670,6 +758,15 @@ mod test {
         let logs = history
             .query()
             .search(to_hex_string(Address::zero()))
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![]);
+
+        let logs = history
+            .query()
+            .state(StateTag::Completed)
             .execute()
             .await
             .unwrap();
@@ -732,6 +829,7 @@ mod test {
         let logs = history
             .query()
             .search(1.to_string())
+            .unwrap()
             .network_id(121)
             .execute()
             .await