diff --git a/backend-rust/.sqlx/query-5487447e97832171ff305531e30004ad48ef1d39db68f80419080d5c3ea9afcf.json b/backend-rust/.sqlx/query-5487447e97832171ff305531e30004ad48ef1d39db68f80419080d5c3ea9afcf.json new file mode 100644 index 00000000..1ff49f95 --- /dev/null +++ b/backend-rust/.sqlx/query-5487447e97832171ff305531e30004ad48ef1d39db68f80419080d5c3ea9afcf.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT MAX(height) as max_height, MIN(height) as min_height\n FROM blocks\n WHERE\n height = $1\n OR starts_with(hash, $2)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "max_height", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "min_height", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + null, + null + ] + }, + "hash": "5487447e97832171ff305531e30004ad48ef1d39db68f80419080d5c3ea9afcf" +} diff --git a/backend-rust/.sqlx/query-9900288397e3ef769e3e264889932596f67aa12452ce59c03a5b7960515c4036.json b/backend-rust/.sqlx/query-9900288397e3ef769e3e264889932596f67aa12452ce59c03a5b7960515c4036.json new file mode 100644 index 00000000..2fe94bcf --- /dev/null +++ b/backend-rust/.sqlx/query-9900288397e3ef769e3e264889932596f67aa12452ce59c03a5b7960515c4036.json @@ -0,0 +1,63 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM (\n SELECT\n hash,\n height,\n slot_time,\n block_time,\n finalization_time,\n baker_id,\n total_amount\n FROM blocks\n WHERE\n height = $5\n OR starts_with(hash, $6)\n AND height > $1\n AND height < $2\n ORDER BY\n (CASE WHEN $4 THEN height END) ASC,\n (CASE WHEN NOT $4 THEN height END) DESC\n LIMIT $3\n ) ORDER BY height DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "hash", + "type_info": "Bpchar" + }, + { + "ordinal": 1, + "name": "height", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "slot_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "block_time", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "finalization_time", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "baker_id", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_amount", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Bool", + "Int8", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + true, + false + ] + }, + "hash": "9900288397e3ef769e3e264889932596f67aa12452ce59c03a5b7960515c4036" +} diff --git a/backend-rust/CHANGELOG.md b/backend-rust/CHANGELOG.md index f35c44d8..f655a812 100644 --- a/backend-rust/CHANGELOG.md +++ b/backend-rust/CHANGELOG.md @@ -12,6 +12,7 @@ Database schema version: 2 - Add index over accounts that delegate stake to a given baker pool. - Add `database_schema_version` and `api_supported_database_schema_version` to `versions` endpoint. - Add database schema version 2 with index over blocks with no `cumulative_finalization_time`, to improve indexing performance. +- Implement `SearchResult::blocks` and add relevant index to database ### Changed diff --git a/backend-rust/Cargo.lock b/backend-rust/Cargo.lock index a42631c6..63935943 100644 --- a/backend-rust/Cargo.lock +++ b/backend-rust/Cargo.lock @@ -983,7 +983,7 @@ dependencies = [ [[package]] name = "concordium-scan" -version = "0.1.19" +version = "0.1.20" dependencies = [ "anyhow", "async-graphql", diff --git a/backend-rust/Cargo.toml b/backend-rust/Cargo.toml index 57ce9c45..cd4d8216 100644 --- a/backend-rust/Cargo.toml +++ b/backend-rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "concordium-scan" -version = "0.1.19" +version = "0.1.20" edition = "2021" description = "CCDScan: Indexer and API for the Concordium blockchain" authors = ["Concordium "] diff --git a/backend-rust/src/graphql_api.rs b/backend-rust/src/graphql_api.rs index a3cea184..325fe5b8 100644 --- a/backend-rust/src/graphql_api.rs +++ b/backend-rust/src/graphql_api.rs @@ -905,14 +905,91 @@ impl SearchResult { async fn blocks( &self, - #[graphql(desc = "Returns the first _n_ elements from the list.")] _first: Option, + ctx: &Context<'_>, + #[graphql(desc = "Returns the first _n_ elements from the list.")] first: Option, #[graphql(desc = "Returns the elements in the list that come after the specified cursor.")] - _after: Option, - #[graphql(desc = "Returns the last _n_ elements from the list.")] _last: Option, + after: Option, + #[graphql(desc = "Returns the last _n_ elements from the list.")] last: Option, #[graphql(desc = "Returns the elements in the list that come before the specified cursor.")] - _before: Option, + before: Option, ) -> ApiResult> { - todo_api!() + let block_hash_regex: Regex = Regex::new(r"^[a-fA-F0-9]{1,64}$") + .map_err(|_| ApiError::InternalError("Invalid regex".to_string()))?; + let pool = get_pool(ctx)?; + let config = get_config(ctx)?; + let query = + ConnectionQuery::::new(first, after, last, before, config.block_connection_limit)?; + let mut connection = connection::Connection::new(false, false); + if !block_hash_regex.is_match(&self.query) { + return Ok(connection); + } + let lower_case_query = self.query.to_lowercase(); + let mut rows = sqlx::query_as!( + Block, + "SELECT * FROM ( + SELECT + hash, + height, + slot_time, + block_time, + finalization_time, + baker_id, + total_amount + FROM blocks + WHERE + height = $5 + OR starts_with(hash, $6) + AND height > $1 + AND height < $2 + ORDER BY + (CASE WHEN $4 THEN height END) ASC, + (CASE WHEN NOT $4 THEN height END) DESC + LIMIT $3 + ) ORDER BY height DESC", + query.from, + query.to, + query.limit, + query.desc, + lower_case_query.parse::().ok(), + lower_case_query + ) + .fetch(pool); + + let mut min_height = None; + let mut max_height = None; + while let Some(block) = rows.try_next().await? { + min_height = Some(match min_height { + None => block.height, + Some(current_min) => min(current_min, block.height), + }); + + max_height = Some(match max_height { + None => block.height, + Some(current_max) => max(current_max, block.height), + }); + connection.edges.push(connection::Edge::new(block.height.to_string(), block)); + } + + if let (Some(page_min_height), Some(page_max_height)) = (min_height, max_height) { + let result = sqlx::query!( + r#" + SELECT MAX(height) as max_height, MIN(height) as min_height + FROM blocks + WHERE + height = $1 + OR starts_with(hash, $2) + "#, + lower_case_query.parse::().ok(), + lower_case_query, + ) + .fetch_one(pool) + .await?; + connection.has_previous_page = + result.max_height.map_or(false, |db_max| db_max > page_max_height); + connection.has_next_page = + result.min_height.map_or(false, |db_min| db_min < page_min_height); + } + Ok(connection) } async fn transactions( diff --git a/backend-rust/src/graphql_api/block.rs b/backend-rust/src/graphql_api/block.rs index 0be86c34..28051379 100644 --- a/backend-rust/src/graphql_api/block.rs +++ b/backend-rust/src/graphql_api/block.rs @@ -106,20 +106,20 @@ impl QueryBlocks { #[derive(Debug, Clone)] pub struct Block { - hash: BlockHash, - height: BlockHeight, + pub(crate) hash: BlockHash, + pub(crate) height: BlockHeight, /// Time of the block being baked. - slot_time: DateTime, + pub(crate) slot_time: DateTime, /// Number of milliseconds between the `slot_time` of this block and its /// parent. - block_time: i32, + pub(crate) block_time: i32, /// If this block is finalized, the number of milliseconds between the /// `slot_time` of this block and the first block that contains a /// finalization proof or quorum certificate that justifies this block /// being finalized. - finalization_time: Option, - baker_id: Option, - total_amount: i64, + pub(crate) finalization_time: Option, + pub(crate) baker_id: Option, + pub(crate) total_amount: i64, } impl Block { diff --git a/backend-rust/src/migrations/m0002-block-cumulative-fin-time-index.sql b/backend-rust/src/migrations/m0002-block-cumulative-fin-time-index.sql index 100c2f8b..d3d097a2 100644 --- a/backend-rust/src/migrations/m0002-block-cumulative-fin-time-index.sql +++ b/backend-rust/src/migrations/m0002-block-cumulative-fin-time-index.sql @@ -3,6 +3,10 @@ CREATE INDEX blocks_height_null_cumulative_finalization_time ON blocks (height) WHERE blocks.cumulative_finalization_time IS NULL AND blocks.finalization_time IS NOT NULL; - +-- blocks_hash_gin_trgm_idx index does not support char +ALTER TABLE blocks ALTER COLUMN hash SET DATA TYPE VARCHAR(64); +-- Used to efficiently perform partial string matching on the `hash` column, +-- allowing fast lookups when searching for blocks by their hash prefix using `LIKE`. +CREATE INDEX blocks_hash_gin_trgm_idx ON blocks USING gin(hash gin_trgm_ops); -- Important for quickly calculating the delegated stake to a baker pool. CREATE INDEX delegated_target_baker_id_index ON accounts(delegated_target_baker_id);