-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce basic migration logic #428
base: main
Are you sure you want to change the base?
Conversation
@@ -0,0 +1,782 @@ | |||
CREATE EXTENSION IF NOT EXISTS pg_trgm; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have this file? It seems to be pretty much the same as what is already there.
I assume it at the very least means we have to remove the old ones?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the old one needs to be removed and I will do that right before merging.
Merging in changes to the old file from main, is just simpler when I keep this around.
@@ -60,6 +64,9 @@ async fn main() -> anyhow::Result<()> { | |||
.connect(&cli.database_url) | |||
.await | |||
.context("Failed constructing database connection pool")?; | |||
// Ensure the database schema is compatible with supported schema version. | |||
migrations::ensure_compatible_schema_version(&pool, SUPPORTED_SCHEMA_VERSION).await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just do the same migration here instead.
During it this way we have an annoying situation where we are dependent on the api is initialising after the migration has occurred on the indexer which is a pretty annoying feature to implement deployment wise.
At least it cannot be done as is, since it happens application side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't think this should be doing migrations.
Most database migrations require changes to the indexer, so if the API starts doing migrations, it might crash it.
New columns need to be populated by the indexer, before the API should start using them anyway.
Removing columns would crash the indexer.
We also don't want several migrations to be running at once, so since we only run one indexer instance, this is probably a better fit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently you are not making changes to the indexer. This is database relevant only. As is these changes will cause failures to the deployment because the SQL schema will inconsistently be incompatible upon startup of the API. The simple solution is to do what I suggested above.
An alternative could be to provide the information about whether the api schema has been applied externally. This could for example be in the health endpoint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A simpler approach yet is to just keep retrying until it is valid instead of failing
@@ -3,7 +3,6 @@ use async_graphql::{scalar, InputValueError, InputValueResult, Scalar, ScalarTyp | |||
|
|||
pub type Amount = UnsignedLong; | |||
pub type TokenId = String; | |||
pub type RewardPeriodLength = i64; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a rebase on top of main, this line and the rust-submodule-link
update should disappear.
PGPASSWORD=${DB_PASSWORD} | ||
|
||
# gRPC interface of the node. | ||
CCDSCAN_INDEXER_GRPC_ENDPOINTS=http://localhost:20000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use as default a public URL node. That would allow external people to easier start the service (even mention the URLs to nodes on all three networks) successfully since people don't need to figure out how to set up a node (or how to change this part to a public node).
CCDSCAN_INDEXER_DATABASE_URL=postgres://postgres:$PGPASSWORD@localhost/ccdscan | ||
|
||
# Database connection used by the ccdscan-api. | ||
CCDSCAN_API_DATABASE_URL=${CCDSCAN_INDEXER_DATABASE_URL} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running make setup && make
(as described in the READMe) overwrites the CCDSCAN_API_DATABASE_URL/PGPASSWORD
string now to an empty string when run for the first time. The make
files need to be tested with these changes.
I am a bit in favor of keeping it simple and telling people to rename the .env_template
file to .env
without providing make
files. Our make
files will get out of date or break earlier or later (if we keep them, we should have some CI pipelines running the make
file long-term).
|
||
## Database schema setup and migrations | ||
|
||
To set up the database schema either from an empty database or migration from an older release of `ccdscan-indexer` run: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add the command to start an empty database
here.
@@ -39,7 +59,7 @@ cargo run --bin ccdscan-indexer -- --node https://grpc.testnet.concordium.com:20 | |||
Note: Since the indexer puts a lot of load on the node, use your own local node whenever possible. | |||
If using the public nodes, run the indexer as short as possible. | |||
|
|||
<!-- TODO When service become stable: add documentation of arguments and environment variables. --> | |||
Both binaries read variables from a `.env` file if present in the directory, use `.env.template` in this project as the starting point. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about the following order to get someone who reads the READme
for the first time up-to-speed as fast as possible:
-
First paragraph includes all dependencies (
## Dependencies
and## Setup for development
paragraph). -
Second paragraph about setups:
- mention the
envs
file (so people know they need to set up envs before running/migrating anything) - e.g. sentence line 62 - mention the database: e.g. how to get an
empty database
set-up (cli command).
- mention the
-
Third paragraph: Migration/Running the services (rest of ReadMe).
``` | ||
|
||
where `<description>` is replaced by a short description of the nature of the migration. | ||
NOTE: Having compile-time checked queries will cause issues, since the queries are invalid until the database have been properly migrated. This is done by _not_ having the `DATABASE_URL` environment variable set until after running the migrations or using: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This Note
could be added to the ccdscan-api
service further up as well with your given cli command as a work around since it can happen in the ccdscan-api
service as well.
#[display("0000:Empty database with no tables yet.")] | ||
Empty = 0, | ||
#[display( | ||
"0001:Initial schema version with tables for blocks, transaction, accounts, contracts, \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"0001:Initial schema version with tables for blocks, transaction, accounts, contracts, \ | |
"0001:Initial schema version with tables for blocks, transactions, accounts, contracts, \ |
"Database is using an older schema version not supported by this version of \ | ||
`ccdscan-api`. | ||
|
||
Use `ccdscan-indexer --migrate` migrate the database schema." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use `ccdscan-indexer --migrate` migrate the database schema." | |
Use `ccdscan-indexer --migrate` to migrate the database schema." |
/// The latest known version of the schema. | ||
const LATEST: SchemaVersion = SchemaVersion::InitialFirstHalf; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This requires to update the LATEST
constant every time we extend the SchemaVersion
enum.
Some alternatives:
- Along the line of nightly Rust feature
std::mem::variant_count
(not available in stable Rust so). - Some helper crates, e.g.:
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
#[derive(Debug, PartialEq, Eq, EnumIter, PartialOrd, Ord, Clone, Copy, derive_more::Display)]
#[repr(i64)]
pub enum SchemaVersion {
#[display("0000:Empty database with no tables yet.")]
Empty,
#[display(
"0001:Initial schema version with tables for blocks, transaction, accounts, contracts, \
tokens and more."
)]
InitialFirstHalf,
}
impl SchemaVersion {
pub fn latest() -> Self {
SchemaVersion::iter().max().unwrap()
}
}
fn from_version(version: i64) -> Option<SchemaVersion> { | ||
match version { | ||
0 => Some(SchemaVersion::Empty), | ||
1 => Some(SchemaVersion::InitialFirstHalf), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You explain in the READMe
well how the SchemaVerrsion
and its functions need to be extended in the future. To be bulletproof against devs extending the SchemaVerrsion
(without having read that part of the READMe
), it would be create if we get compile-errors
when extending the SchemaVersion
enum and forget to update any of the functions here.
#[repr(i64)] | ||
pub enum SchemaVersion { | ||
#[display("0000:Empty database with no tables yet.")] | ||
Empty = 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty = 0, | |
Empty , |
/// Query the migrations table for any migrations which have been destructive | ||
/// since the provided version. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
if let Some(destructive_migration) = destructive_migration { | ||
anyhow::bail!( | ||
"Database is using a newer schema version, which is not compatible with the current \ | ||
version: | ||
Support {} | ||
Found {}", | ||
Migration::from(supported), | ||
destructive_migration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to return all breaking changes
(even practically it is unlikely we have more than one) and distinguish between supported
, current
and breaking versions
.
if let Some(destructive_migration) = destructive_migration { | |
anyhow::bail!( | |
"Database is using a newer schema version, which is not compatible with the current \ | |
version: | |
Support {} | |
Found {}", | |
Migration::from(supported), | |
destructive_migration | |
if let Some(destructive_migration:Vec<Migrations>) = destructive_migrations { | |
anyhow::bail!( | |
"Database is using a newer schema version which is not compatible with the supported \ | |
version of this service. The following breaking schema migration have happened: | |
Found {} | |
Support {} | |
List of breaking schema versions: {}", | |
current, | |
Migration::from(supported), | |
destructive_versions |
"0001:Initial schema version with tables for blocks, transaction, accounts, contracts, \ | ||
tokens and more." | ||
)] | ||
InitialFirstHalf = 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
InitialFirstHalf = 1, | |
InitialFirstHalf, |
fn from_version(version: i64) -> Option<SchemaVersion> { | ||
match version { | ||
0 => Some(SchemaVersion::Empty), | ||
1 => Some(SchemaVersion::InitialFirstHalf), | ||
_ => None, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementing the try_from
trait might be more common than a custom conversion function.
fn from_version(version: i64) -> Option<SchemaVersion> { | |
match version { | |
0 => Some(SchemaVersion::Empty), | |
1 => Some(SchemaVersion::InitialFirstHalf), | |
_ => None, | |
} | |
} | |
impl TryFrom<i64> for SchemaVersion { | |
type Error = anyhow::Error; | |
fn try_from(version: i64) -> Result<Self, Self::Error> { | |
match version { | |
0 => Ok(SchemaVersion::Empty), | |
1 => Ok(SchemaVersion::InitialFirstHalf), | |
_ => anyhow::bail!("Unknown database schema version"), | |
} | |
} | |
} |
/// Run migrations for this schema version to the next. | ||
async fn migration_to_next(&self, pool: &PgPool) -> anyhow::Result<SchemaVersion> { | ||
let mut tx = pool.begin().await?; | ||
let start_time = chrono::Utc::now(); | ||
let new_version = match self { | ||
SchemaVersion::Empty => { | ||
// Set up the initial database schema. | ||
tx.as_mut() | ||
.execute(sqlx::raw_sql(include_str!("./migrations/m0001-initialize.sql"))) | ||
.await?; | ||
SchemaVersion::InitialFirstHalf | ||
} | ||
SchemaVersion::InitialFirstHalf => unimplemented!( | ||
"No migration implemented for database schema version {}", | ||
self.as_i64() | ||
), | ||
}; | ||
let end_time = chrono::Utc::now(); | ||
insert_migration(&mut tx, &new_version.into(), start_time, end_time).await?; | ||
tx.commit().await?; | ||
Ok(new_version) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Run migrations for this schema version to the next. | |
async fn migration_to_next(&self, pool: &PgPool) -> anyhow::Result<SchemaVersion> { | |
let mut tx = pool.begin().await?; | |
let start_time = chrono::Utc::now(); | |
let new_version = match self { | |
SchemaVersion::Empty => { | |
// Set up the initial database schema. | |
tx.as_mut() | |
.execute(sqlx::raw_sql(include_str!("./migrations/m0001-initialize.sql"))) | |
.await?; | |
SchemaVersion::InitialFirstHalf | |
} | |
SchemaVersion::InitialFirstHalf => unimplemented!( | |
"No migration implemented for database schema version {}", | |
self.as_i64() | |
), | |
}; | |
let end_time = chrono::Utc::now(); | |
insert_migration(&mut tx, &new_version.into(), start_time, end_time).await?; | |
tx.commit().await?; | |
Ok(new_version) | |
} | |
fn next_version(self) -> anyhow::Result<SchemaVersion> { | |
(self.as_i64() + 1).try_into() | |
} | |
/// Run migrations for this schema version to the next. | |
async fn migration_to_next(&self, pool: &PgPool) -> anyhow::Result<SchemaVersion> { | |
let mut tx = pool.begin().await?; | |
let start_time = chrono::Utc::now(); | |
match self { | |
SchemaVersion::Empty => { | |
// Set up the initial database schema. | |
tx.as_mut() | |
.execute(sqlx::raw_sql(include_str!("./migrations/m0001-initialize.sql"))) | |
.await?; | |
} | |
SchemaVersion::InitialFirstHalf => unimplemented!( | |
"No migration implemented for database schema version {}", | |
self.as_i64() | |
), | |
}; | |
let new_version = self.next_version()?; | |
let end_time = chrono::Utc::now(); | |
insert_migration(&mut tx, &new_version.into(), start_time, end_time).await?; | |
tx.commit().await?; | |
Ok(new_version) | |
} |
Purpose
Introduce migration logic into the
ccdscan-indexer
binary, allowing us to run migrations from rust.ccdscan-api
now ensures the database schema version is compatible andccdscan-indexer
ensures it is the latest before running.Changes
DATABASE_URL
environment variable for both binaries to allow for disabling compile-time checked queries while running the service and to follow the naming convention..env.template
with more settings and comments