diff --git a/backend-rust/.sqlx/query-163cc788cf5c4fd22a1f8e0289ad322fa7d92a7dd503a03a517622f2942c4d60.json b/backend-rust/.sqlx/query-163cc788cf5c4fd22a1f8e0289ad322fa7d92a7dd503a03a517622f2942c4d60.json new file mode 100644 index 00000000..ebd4ef0f --- /dev/null +++ b/backend-rust/.sqlx/query-163cc788cf5c4fd22a1f8e0289ad322fa7d92a7dd503a03a517622f2942c4d60.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COALESCE(MAX(index), 0) FROM accounts", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "coalesce", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "163cc788cf5c4fd22a1f8e0289ad322fa7d92a7dd503a03a517622f2942c4d60" +} diff --git a/backend-rust/.sqlx/query-792aeb42f25e1f70758407b469945187b00c166757ad7f9324abf3f58ac1e821.json b/backend-rust/.sqlx/query-792aeb42f25e1f70758407b469945187b00c166757ad7f9324abf3f58ac1e821.json new file mode 100644 index 00000000..c67948eb --- /dev/null +++ b/backend-rust/.sqlx/query-792aeb42f25e1f70758407b469945187b00c166757ad7f9324abf3f58ac1e821.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "-- Counts accounts in buckets by counting the cumulative total number of\n-- accounts (i.e. the account index) at or before (i.e. <=) the start of the\n-- bucket and the same number just before (i.e. <) the next bucket. The\n-- difference between the two numbers should give the total number of accounts\n-- created within the bucket.\nSELECT\n -- The bucket time is the starting time of the bucket.\n bucket_time,\n -- Number of accounts at or before the bucket.\n COALESCE(before_bucket.index, 0) as start_index,\n -- Number of accounts at the end of the bucket.\n COALESCE(after_bucket.index, 0) as end_index\nFROM\n -- We generate a time series of all the buckets where accounts will be counted.\n -- $1 is the full period, $2 is the bucket interval.\n -- For the rest of the comments, let's go with the example of a full period of 7 days with 6 hour buckets.\n generate_series(\n -- The first bucket starts 7 days ago.\n now() - $1::interval,\n -- The final bucket starts 6 hours ago, since the bucket time is the start of the bucket.\n now() - $2::interval,\n -- Each bucket is seperated by 6 hours.\n $2::interval\n ) AS bucket_time\nLEFT JOIN LATERAL (\n -- Selects the index at or before the start of the bucket.\n SELECT index\n FROM accounts\n LEFT JOIN blocks ON created_block = height\n WHERE slot_time <= bucket_time\n ORDER BY slot_time DESC\n LIMIT 1\n) before_bucket ON true\nLEFT JOIN LATERAL (\n -- Selects the index at the end of the bucket.\n SELECT index\n FROM accounts\n LEFT JOIN blocks ON created_block = height\n WHERE slot_time < bucket_time + $2::interval\n ORDER BY slot_time DESC\n LIMIT 1\n) after_bucket ON true\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "bucket_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 1, + "name": "start_index", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "end_index", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Interval", + "Interval" + ] + }, + "nullable": [ + null, + null, + null + ] + }, + "hash": "792aeb42f25e1f70758407b469945187b00c166757ad7f9324abf3f58ac1e821" +} diff --git a/backend-rust/.sqlx/query-d6bccf668d544b1649962a5f0fb358bada7fbdbb4f78de19cc2ab8d0be5a83d5.json b/backend-rust/.sqlx/query-d6bccf668d544b1649962a5f0fb358bada7fbdbb4f78de19cc2ab8d0be5a83d5.json new file mode 100644 index 00000000..0e93971e --- /dev/null +++ b/backend-rust/.sqlx/query-d6bccf668d544b1649962a5f0fb358bada7fbdbb4f78de19cc2ab8d0be5a83d5.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COALESCE(MAX(index), 0)\n FROM accounts\n LEFT JOIN blocks ON created_block = height\n WHERE slot_time < (now() - $1::interval)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "coalesce", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Interval" + ] + }, + "nullable": [ + null + ] + }, + "hash": "d6bccf668d544b1649962a5f0fb358bada7fbdbb4f78de19cc2ab8d0be5a83d5" +} diff --git a/backend-rust/migrations/0001_initialize.up.sql b/backend-rust/migrations/0001_initialize.up.sql index 72bc0ebf..4c7b750f 100644 --- a/backend-rust/migrations/0001_initialize.up.sql +++ b/backend-rust/migrations/0001_initialize.up.sql @@ -208,6 +208,9 @@ CREATE TABLE accounts( -- credential_registration_id ); +-- Important for performance when joining accounts with its associated creation block. +CREATE INDEX accounts_created_block_idx ON accounts (created_block); + -- Add foreign key constraint now that the account table is created. ALTER TABLE transactions ADD CONSTRAINT fk_transaction_sender diff --git a/backend-rust/src/graphql_api.rs b/backend-rust/src/graphql_api.rs index 7dc46c3c..2e6eaa41 100644 --- a/backend-rust/src/graphql_api.rs +++ b/backend-rust/src/graphql_api.rs @@ -5,6 +5,7 @@ #![allow(unused_variables)] +mod account_metrics; mod transaction_metrics; // TODO remove this macro, when done with first iteration @@ -15,6 +16,7 @@ macro_rules! todo_api { }; } +use account_metrics::AccountMetricsQuery; use anyhow::Context as _; use async_graphql::{ http::GraphiQLSource, @@ -63,7 +65,7 @@ pub struct ApiServiceConfig { } #[derive(MergedObject, Default)] -pub struct Query(BaseQuery, TransactionMetricsQuery); +pub struct Query(BaseQuery, AccountMetricsQuery, TransactionMetricsQuery); pub struct Service { pub schema: Schema, @@ -754,7 +756,6 @@ LIMIT 30", // WHERE slot_time > (LOCALTIMESTAMP - $1::interval) }) } - // accountsMetrics(period: MetricsPeriod!): AccountsMetrics // bakerMetrics(period: MetricsPeriod!): BakerMetrics! // rewardMetrics(period: MetricsPeriod!): RewardMetrics! // rewardMetricsForAccount(accountId: ID! period: MetricsPeriod!): diff --git a/backend-rust/src/graphql_api/account_metrics.rs b/backend-rust/src/graphql_api/account_metrics.rs new file mode 100644 index 00000000..96b0dfbf --- /dev/null +++ b/backend-rust/src/graphql_api/account_metrics.rs @@ -0,0 +1,113 @@ +use std::sync::Arc; + +use async_graphql::{Context, Object, SimpleObject}; +use sqlx::postgres::types::PgInterval; + +use super::{get_pool, ApiError, ApiResult, DateTime, MetricsPeriod, TimeSpan}; + +#[derive(SimpleObject)] +struct AccountMetrics { + /// Total number of accounts created (all time). + last_cumulative_accounts_created: i64, + + /// Total number of accounts created in requested period. + accounts_created: i64, + + buckets: AccountMetricsBuckets, +} + +#[derive(SimpleObject)] +struct AccountMetricsBuckets { + /// The width (time interval) of each bucket. + bucket_width: TimeSpan, + + /// Start of the bucket time period. Intended x-axis value. + #[graphql(name = "x_Time")] + x_time: Vec, + + /// Total number of accounts created (all time) at the end of the bucket + /// period. Intended y-axis value. + #[graphql(name = "y_LastCumulativeAccountsCreated")] + y_last_cumulative_accounts_created: Vec, + + /// Number of accounts created within bucket time period. Intended y-axis + /// value. + #[graphql(name = "y_AccountsCreated")] + y_accounts_created: Vec, +} + +#[derive(Default)] +pub(crate) struct AccountMetricsQuery; + +#[Object] +impl AccountMetricsQuery { + async fn account_metrics( + &self, + ctx: &Context<'_>, + period: MetricsPeriod, + ) -> ApiResult { + let pool = get_pool(ctx)?; + + let last_cumulative_accounts_created = + sqlx::query_scalar!("SELECT COALESCE(MAX(index), 0) FROM accounts") + .fetch_one(pool) + .await? + .expect("coalesced"); + + // The full period interval, e.g. 7 days. + let period_interval: PgInterval = period + .as_duration() + .try_into() + .map_err(|e| ApiError::DurationOutOfRange(Arc::new(e)))?; + + let cumulative_accounts_created_before_period = sqlx::query_scalar!( + "SELECT COALESCE(MAX(index), 0) + FROM accounts + LEFT JOIN blocks ON created_block = height + WHERE slot_time < (now() - $1::interval)", + period_interval, + ) + .fetch_one(pool) + .await? + .expect("coalesced"); + + let accounts_created = + last_cumulative_accounts_created - cumulative_accounts_created_before_period; + + let bucket_width = period.bucket_width(); + + // The bucket interval, e.g. 6 hours. + let bucket_interval: PgInterval = + bucket_width.try_into().map_err(|err| ApiError::DurationOutOfRange(Arc::new(err)))?; + + let rows = sqlx::query_file!( + "src/graphql_api/account_metrics.sql", + period_interval, + bucket_interval, + ) + .fetch_all(pool) + .await?; + + let x_time = rows + .iter() + .map(|r| r.bucket_time.expect("generated by generate_series so never null")) + .collect(); + let y_last_cumulative_accounts_created = + rows.iter().map(|r| r.end_index.expect("coalesced")).collect(); + let y_accounts_created = rows + .iter() + .map(|r| r.end_index.expect("coalesced") - r.start_index.expect("coalesced")) + .collect(); + + Ok(AccountMetrics { + last_cumulative_accounts_created, + accounts_created, + buckets: AccountMetricsBuckets { + bucket_width: TimeSpan(bucket_width), + x_time, + y_last_cumulative_accounts_created, + y_accounts_created, + }, + }) + } +} diff --git a/backend-rust/src/graphql_api/account_metrics.sql b/backend-rust/src/graphql_api/account_metrics.sql new file mode 100644 index 00000000..5fc18436 --- /dev/null +++ b/backend-rust/src/graphql_api/account_metrics.sql @@ -0,0 +1,42 @@ +-- Counts accounts in buckets by counting the cumulative total number of +-- accounts (i.e. the account index) at or before (i.e. <=) the start of the +-- bucket and the same number just before (i.e. <) the next bucket. The +-- difference between the two numbers should give the total number of accounts +-- created within the bucket. +SELECT + -- The bucket time is the starting time of the bucket. + bucket_time, + -- Number of accounts at or before the bucket. + COALESCE(before_bucket.index, 0) as start_index, + -- Number of accounts at the end of the bucket. + COALESCE(after_bucket.index, 0) as end_index +FROM + -- We generate a time series of all the buckets where accounts will be counted. + -- $1 is the full period, $2 is the bucket interval. + -- For the rest of the comments, let's go with the example of a full period of 7 days with 6 hour buckets. + generate_series( + -- The first bucket starts 7 days ago. + now() - $1::interval, + -- The final bucket starts 6 hours ago, since the bucket time is the start of the bucket. + now() - $2::interval, + -- Each bucket is seperated by 6 hours. + $2::interval + ) AS bucket_time +LEFT JOIN LATERAL ( + -- Selects the index at or before the start of the bucket. + SELECT index + FROM accounts + LEFT JOIN blocks ON created_block = height + WHERE slot_time <= bucket_time + ORDER BY slot_time DESC + LIMIT 1 +) before_bucket ON true +LEFT JOIN LATERAL ( + -- Selects the index at the end of the bucket. + SELECT index + FROM accounts + LEFT JOIN blocks ON created_block = height + WHERE slot_time < bucket_time + $2::interval + ORDER BY slot_time DESC + LIMIT 1 +) after_bucket ON true