From fc4aa5aeb87f4258e4efb6b77e01bbb8d16e3e57 Mon Sep 17 00:00:00 2001 From: Maksim Dimitrov Date: Mon, 20 Oct 2025 15:04:10 +0300 Subject: [PATCH 1/3] graph, store, server: Expose table sizes in deployment indexing status Signed-off-by: Maksim Dimitrov --- graph/src/data/subgraph/status.rs | 34 +++++++++++++ server/index-node/src/schema.graphql | 11 ++++ store/postgres/src/detail.rs | 75 +++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/graph/src/data/subgraph/status.rs b/graph/src/data/subgraph/status.rs index e2c14751955..5d78311249d 100644 --- a/graph/src/data/subgraph/status.rs +++ b/graph/src/data/subgraph/status.rs @@ -91,6 +91,35 @@ impl IntoValue for ChainInfo { } } +#[derive(Debug, Default)] +pub struct SubgraphSize { + pub row_estimate: i64, + pub table_bytes: i64, + pub index_bytes: i64, + pub toast_bytes: i64, + pub total_bytes: i64, +} + +impl IntoValue for SubgraphSize { + fn into_value(self) -> r::Value { + let SubgraphSize { + row_estimate, + table_bytes, + index_bytes, + toast_bytes, + total_bytes, + } = self; + object! { + __typename: "SubgraphSize", + rowEstimate: format!("{}", row_estimate), + tableSize: format!("{}", table_bytes), + indexSize: format!("{}", index_bytes), + toastSize: format!("{}", toast_bytes), + totalSize: format!("{}", total_bytes), + } + } +} + #[derive(Debug)] pub struct Info { pub id: DeploymentId, @@ -114,6 +143,9 @@ pub struct Info { pub node: Option, pub history_blocks: i32, + + /// The size of all entity tables in a deployment's namespace in bytes. + pub subgraph_size: SubgraphSize, } impl IntoValue for Info { @@ -130,6 +162,7 @@ impl IntoValue for Info { non_fatal_errors, synced, history_blocks, + subgraph_size, } = self; fn subgraph_error_to_value(subgraph_error: SubgraphError) -> r::Value { @@ -173,6 +206,7 @@ impl IntoValue for Info { entityCount: format!("{}", entity_count), node: node, historyBlocks: history_blocks, + subgraphSize: subgraph_size.into_value(), } } } diff --git a/server/index-node/src/schema.graphql b/server/index-node/src/schema.graphql index 4179cabad8c..10475d75a0d 100644 --- a/server/index-node/src/schema.graphql +++ b/server/index-node/src/schema.graphql @@ -77,6 +77,9 @@ type SubgraphIndexingStatus { paused: Boolean historyBlocks: Int! + + "Total size of all tables a deployment's namespace in bytes." + subgraphSize: SubgraphSize! } interface ChainIndexingStatus { @@ -206,3 +209,11 @@ type ApiVersion { """ version: String! } + +type SubgraphSize { + rowEstimate: BigInt! + tableSize: BigInt! + indexSize: BigInt! + toastSize: BigInt! + totalSize: BigInt! +} diff --git a/store/postgres/src/detail.rs b/store/postgres/src/detail.rs index 0be3909a2c9..d4724834426 100644 --- a/store/postgres/src/detail.rs +++ b/store/postgres/src/detail.rs @@ -7,6 +7,7 @@ use diesel::prelude::{ ExpressionMethods, JoinOnDsl, NullableExpressionMethods, OptionalExtension, PgConnection, QueryDsl, RunQueryDsl, SelectableHelper as _, }; +use diesel::sql_types::{Array, BigInt, Integer}; use diesel_derives::Associations; use git_testament::{git_testament, git_testament_macros}; use graph::blockchain::BlockHash; @@ -239,6 +240,7 @@ pub(crate) fn info_from_details( non_fatal: Vec, sites: &[Arc], subgraph_history_blocks: i32, + subgraph_size: status::SubgraphSize, ) -> Result { let DeploymentDetail { id, @@ -301,6 +303,7 @@ pub(crate) fn info_from_details( entity_count, node: None, history_blocks: subgraph_history_blocks, + subgraph_size, }) } @@ -422,17 +425,87 @@ pub(crate) fn deployment_statuses( .collect() }; + let mut deployment_sizes = deployment_sizes(conn, sites)?; + details_with_fatal_error .into_iter() .map(|(deployment, head, fatal)| { let detail = DeploymentDetail::from((deployment, head)); let non_fatal = non_fatal_errors.remove(&detail.id).unwrap_or_default(); let subgraph_history_blocks = history_blocks_map.remove(&detail.id).unwrap_or_default(); - info_from_details(detail, fatal, non_fatal, sites, subgraph_history_blocks) + let table_sizes = deployment_sizes.remove(&detail.id).unwrap_or_default(); + info_from_details( + detail, + fatal, + non_fatal, + sites, + subgraph_history_blocks, + table_sizes, + ) }) .collect() } +fn deployment_sizes( + conn: &mut PgConnection, + sites: &[Arc], +) -> Result, StoreError> { + #[derive(QueryableByName)] + struct SubgraphSizeRow { + #[diesel(sql_type = Integer)] + id: DeploymentId, + #[diesel(sql_type = BigInt)] + row_estimate: i64, + #[diesel(sql_type = BigInt)] + table_bytes: i64, + #[diesel(sql_type = BigInt)] + index_bytes: i64, + #[diesel(sql_type = BigInt)] + toast_bytes: i64, + #[diesel(sql_type = BigInt)] + total_bytes: i64, + } + + let mut query = String::from( + r#" + SELECT + ds.id, + ss.row_estimate::bigint, + ss.table_bytes::bigint, + ss.index_bytes::bigint, + ss.toast_bytes::bigint, + ss.total_bytes::bigint + FROM deployment_schemas ds + JOIN info.subgraph_sizes as ss on ss.name = ds.name + "#, + ); + + let rows = if sites.is_empty() { + diesel::sql_query(query).load::(conn)? + } else { + query.push_str(" WHERE ds.id = ANY($1)"); + diesel::sql_query(query) + .bind::, _>(sites.iter().map(|site| site.id).collect::>()) + .load::(conn)? + }; + + let mut sizes: HashMap = HashMap::new(); + for row in rows { + sizes.insert( + row.id, + status::SubgraphSize { + row_estimate: row.row_estimate, + table_bytes: row.table_bytes, + index_bytes: row.index_bytes, + toast_bytes: row.toast_bytes, + total_bytes: row.total_bytes, + }, + ); + } + + Ok(sizes) +} + #[derive(Queryable, Selectable, Identifiable, Associations)] #[diesel(table_name = subgraph_manifest)] #[diesel(belongs_to(GraphNodeVersion))] From 7740c0aa0e4e1d2617887e3e3d6c14de49016ba9 Mon Sep 17 00:00:00 2001 From: Maksim Dimitrov Date: Mon, 20 Oct 2025 15:06:22 +0300 Subject: [PATCH 2/3] server: Make the indexingStatuses argument required Signed-off-by: Maksim Dimitrov --- server/index-node/src/resolver.rs | 62 ++++++++++++++-------------- server/index-node/src/schema.graphql | 2 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/server/index-node/src/resolver.rs b/server/index-node/src/resolver.rs index f1b5b4ecab6..9394b370920 100644 --- a/server/index-node/src/resolver.rs +++ b/server/index-node/src/resolver.rs @@ -123,7 +123,7 @@ impl IndexNodeResolver { } fn resolve_indexing_statuses(&self, field: &a::Field) -> Result { - let deployments = field + let deployments: Vec = field .argument_value("subgraphs") .map(|value| match value { r::Value::List(ids) => ids @@ -135,7 +135,7 @@ impl IndexNodeResolver { .collect(), _ => unreachable!(), }) - .unwrap_or_else(Vec::new); + .unwrap(); if deployments.is_empty() { return Ok(r::Value::List(vec![])); @@ -171,6 +171,35 @@ impl IndexNodeResolver { Ok(infos.into_value()) } + fn resolve_indexing_status_for_version( + &self, + field: &a::Field, + + // If `true` return the current version, if `false` return the pending version. + current_version: bool, + ) -> Result { + // We can safely unwrap because the argument is non-nullable and has been validated. + let subgraph_name = field.get_required::("subgraphName").unwrap(); + + debug!( + self.logger, + "Resolve indexing status for subgraph name"; + "name" => &subgraph_name, + "current_version" => current_version, + ); + + let infos = self.store.status(status::Filter::SubgraphVersion( + subgraph_name, + current_version, + ))?; + + Ok(infos + .into_iter() + .next() + .map(|info| info.into_value()) + .unwrap_or(r::Value::Null)) + } + fn resolve_entity_changes_in_block( &self, field: &a::Field, @@ -443,35 +472,6 @@ impl IndexNodeResolver { Ok(r::Value::List(public_poi_results)) } - fn resolve_indexing_status_for_version( - &self, - field: &a::Field, - - // If `true` return the current version, if `false` return the pending version. - current_version: bool, - ) -> Result { - // We can safely unwrap because the argument is non-nullable and has been validated. - let subgraph_name = field.get_required::("subgraphName").unwrap(); - - debug!( - self.logger, - "Resolve indexing status for subgraph name"; - "name" => &subgraph_name, - "current_version" => current_version, - ); - - let infos = self.store.status(status::Filter::SubgraphVersion( - subgraph_name, - current_version, - ))?; - - Ok(infos - .into_iter() - .next() - .map(|info| info.into_value()) - .unwrap_or(r::Value::Null)) - } - async fn validate_and_extract_features( subgraph_store: &Arc, unvalidated_subgraph_manifest: UnvalidatedSubgraphManifest, diff --git a/server/index-node/src/schema.graphql b/server/index-node/src/schema.graphql index 10475d75a0d..50a35bfbcb6 100644 --- a/server/index-node/src/schema.graphql +++ b/server/index-node/src/schema.graphql @@ -21,7 +21,7 @@ type Query { indexingStatusesForSubgraphName( subgraphName: String! ): [SubgraphIndexingStatus!]! - indexingStatuses(subgraphs: [String!]): [SubgraphIndexingStatus!]! + indexingStatuses(subgraphs: [String!]!): [SubgraphIndexingStatus!]! proofOfIndexing( subgraph: String! blockNumber: Int! From b1b19cc05f1d2b2811686322637ad2c5d04a3ad9 Mon Sep 17 00:00:00 2001 From: Maksim Dimitrov Date: Mon, 20 Oct 2025 16:44:22 +0300 Subject: [PATCH 3/3] store: Handle materialized view not being populated Signed-off-by: Maksim Dimitrov --- store/postgres/src/detail.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/store/postgres/src/detail.rs b/store/postgres/src/detail.rs index d4724834426..ce76206483b 100644 --- a/store/postgres/src/detail.rs +++ b/store/postgres/src/detail.rs @@ -480,13 +480,19 @@ fn deployment_sizes( "#, ); - let rows = if sites.is_empty() { - diesel::sql_query(query).load::(conn)? + let result = if sites.is_empty() { + diesel::sql_query(query).load::(conn) } else { query.push_str(" WHERE ds.id = ANY($1)"); diesel::sql_query(query) .bind::, _>(sites.iter().map(|site| site.id).collect::>()) - .load::(conn)? + .load::(conn) + }; + + let rows = match result { + Ok(rows) => rows, + Err(e) if e.to_string().contains("has not been populated") => Vec::new(), + Err(e) => return Err(e.into()), }; let mut sizes: HashMap = HashMap::new();