Skip to content

Commit

Permalink
feat(db): add database version management system (#431)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbcaron authored Jan 3, 2025
1 parent 95fcd78 commit 922d7a8
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 11 deletions.
4 changes: 4 additions & 0 deletions .db-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
current_version: 0
versions:
- version: 0
pr: 372
7 changes: 6 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!--- Please provide a general summary of your changes in the title above -->

# Pull Request type
## Pull Request type

<!-- Please try to limit your pull request to one type; submit multiple pull requests if needed. -->

Expand Down Expand Up @@ -33,6 +33,11 @@ Resolves: #NA

<!-- Yes or No -->
<!-- If this does introduce a breaking change, please describe the impact and migration path for existing applications below. -->
<!-- If you modify database schema, ensure you:
1. Add the 'bump_db' label to the PR
2. Document the schema changes
3. Provide migration instructions if needed
-->

## Other information

Expand Down
3 changes: 3 additions & 0 deletions .github/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
- name: "breaking-change"
color: ee0701
description: "A change that changes the API or breaks backward compatibility for users."
- name: "bump_db"
color: ee0701
description: "Changes requiring a database version increment."
- name: "bugfix"
color: ee0701
description:
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/db-version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: DB Version Management

on:
workflow_dispatch:
workflow_call:

jobs:
update-db-version:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'bump_db')
steps:
- uses: actions/checkout@v3

- name: Install yq
run: sudo apt-get install -y yq

- name: Check if PR already bumped
id: check_bump
run: |
PR_NUM="${{ github.event.pull_request.number }}"
if yq -e ".versions[] | select(.pr == ${PR_NUM})" .db-versions.yml > /dev/null 2>&1; then
echo "already_bumped=true" >> $GITHUB_OUTPUT
else
echo "already_bumped=false" >> $GITHUB_OUTPUT
fi
- name: Configure Git
if: steps.check_bump.outputs.already_bumped == 'false'
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Update DB Version
if: steps.check_bump.outputs.already_bumped == 'false'
run: |
./scripts/update-db-version.sh "${{ github.event.pull_request.number }}"
- name: Commit and Push
if: steps.check_bump.outputs.already_bumped == 'false'
run: |
if [[ -n "$(git status --porcelain)" ]]; then
git add .db-versions.toml
git commit -m "chore: bump db version"
git push origin HEAD
fi
21 changes: 15 additions & 6 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ permissions:
pull-requests: write

jobs:
update_db_version:
name: Update DB Version
if: github.event.pull_request.draft == false
uses: ./.github/workflows/db-version.yml

linters:
name: Run linters
if: github.event.pull_request.draft == false
needs: update_db_version
if: ${{ always() }}
uses: ./.github/workflows/linters.yml

rust_check:
Expand All @@ -26,21 +32,24 @@ jobs:

linters_cargo:
name: Run Cargo linters
if: github.event.pull_request.draft == false
uses: ./.github/workflows/linters-cargo.yml
needs: rust_check
uses: ./.github/workflows/linters-cargo.yml

coverage:
name: Run Coverage
if: github.event.pull_request.draft == false
uses: ./.github/workflows/coverage.yml
needs: update_db_version
if: ${{ always() }}
secrets: inherit
uses: ./.github/workflows/coverage.yml

build:
name: Build Madara
needs: update_db_version
if: ${{ always() }}
uses: ./.github/workflows/build.yml

js_test:
name: Run JS Tests
uses: ./.github/workflows/starknet-js-test.yml
needs: build
if: ${{ always() }}
uses: ./.github/workflows/starknet-js-test.yml
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Madara is a powerful Starknet client written in Rust.
- [Madara-specific JSON-RPC Methods](#madara-specific-json-rpc-methods)
- [Example of Calling a JSON-RPC Method](#example-of-calling-a-json-rpc-method)
- 📚 [Database Migration](#-database-migration)
- [Database Version Management](#database-version-management)
- [Warp Update](#warp-update)
- [Running without `--warp-update-sender`](#running-without---warp-update-sender)
-[Supported Features](#-supported-features)
Expand Down Expand Up @@ -545,10 +546,36 @@ which is returned with each websocket response.
[⬅️ back to top](#-madara-starknet-client)
When migration to a newer version of Madara you might need to update your
database. Instead of re-synchronizing the entirety of your chain's state from
genesis, you can use Madara's **warp update** feature. This is essentially a
form of trusted sync with better performances as it is run from a local source.
### Database Version Management
The database version management system ensures compatibility between Madara's
binary and database versions.
When you encounter a version mismatch error, it means your database schema needs
to be updated to match your current binary version.
When you see:
```console
Error: Database version 41 is not compatible with current binary. Expected version 42
```
This error indicates that:
1. Your current binary requires database version 42
2. Your database is still at version 41
3. Migration is required before you can continue
> [!IMPORTANT]
> Don't panic! Your data is safe, but you need to migrate it before continuing.
To migrate your database, you have two options:
1. Use Madara's **warp update** feature (recommended)
2. Re-synchronize from genesis (not recommended)
The warp update feature provides a trusted sync from a local source, offering
better performance than re-synchronizing the entirety of your chain's state
from genesis.
### Warp Update
Expand Down
145 changes: 145 additions & 0 deletions crates/madara/client/db/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//! Database version management for build-time validation
//!
//! This build script:
//! 1. Reads the current database version from `.db-versions.yml` in project root
//! 2. Injects it as `DB_VERSION` environment variable for runtime checks
//! 3. Ensures version file is well-formatted
//!
//! # File format
//! The version file must be a YAML file with the following structure:
//! ```yaml
//! current_version: 42
//! versions:
//! - version: 42
//! pr: 123
//! - version: 41
//! pr: 120
//! ```
//!
//! # Environment variables
//! - `CARGO_MANIFEST_DIR`: Set by cargo, path to the current crate
//!
//! # Outputs
//! - `cargo:rustc-env=DB_VERSION=X`: Current database version
//! - `cargo:rerun-if-changed=.db-versions.yml`: Rebuild if version changes
//!
//! # Errors
//! Fails the build if:
//! - Version file is missing or malformed
//! - Version number cannot be parsed
//! - Cannot find project root directory
use std::borrow::Cow;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

const DB_VERSION_FILE: &str = ".db-versions.yml";
const PARENT_LEVELS: usize = 4;

#[allow(clippy::print_stderr)]
fn main() {
if let Err(e) = get_db_version() {
eprintln!("Failed to get DB version: {}", e);
std::process::exit(1);
}
}

#[derive(Debug)]
enum BuildError {
EnvVar(env::VarError),
Io(std::io::Error),
Parse(Cow<'static, str>),
}

impl std::fmt::Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildError::EnvVar(e) => write!(f, "Environment variable error: {}", e),
BuildError::Io(e) => write!(f, "IO error: {}", e),
BuildError::Parse(msg) => write!(f, "Parse error: {}", msg),
}
}
}

impl From<env::VarError> for BuildError {
fn from(e: env::VarError) -> Self {
BuildError::EnvVar(e)
}
}

impl From<std::io::Error> for BuildError {
fn from(e: std::io::Error) -> Self {
BuildError::Io(e)
}
}

fn get_db_version() -> Result<(), BuildError> {
let manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
let root_dir = get_parents(&PathBuf::from(manifest_dir), PARENT_LEVELS)?;
let file_path = root_dir.join(DB_VERSION_FILE);

let content = fs::read_to_string(&file_path).map_err(|e| {
BuildError::Io(std::io::Error::new(e.kind(), format!("Failed to read {}: {}", file_path.display(), e)))
})?;

let current_version = parse_version(&content)?;

println!("cargo:rerun-if-changed={}", DB_VERSION_FILE);
println!("cargo:rustc-env=DB_VERSION={}", current_version);

Ok(())
}

fn parse_version(content: &str) -> Result<u32, BuildError> {
content
.lines()
.find(|line| line.starts_with("current_version:"))
.ok_or_else(|| BuildError::Parse(Cow::Borrowed("Could not find current_version")))?
.split(':')
.nth(1)
.ok_or_else(|| BuildError::Parse(Cow::Borrowed("Invalid current_version format")))?
.trim()
.parse()
.map_err(|_| BuildError::Parse(Cow::Borrowed("Could not parse current_version as u32")))
}

fn get_parents(path: &Path, n: usize) -> Result<PathBuf, BuildError> {
let mut path = path.to_path_buf();
for _ in 0..n {
path = path
.parent()
.ok_or(BuildError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "Parent not found")))?
.to_path_buf();
}
Ok(path)
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;

#[test]
fn test_parse_version_valid() {
let content = "current_version: 42\nother: stuff";
assert_eq!(parse_version(content).unwrap(), 42);
}

#[test]
fn test_parse_version_invalid_format() {
let content = "wrong_format";
assert!(matches!(parse_version(content), Err(BuildError::Parse(_))));
}

#[test]
fn test_get_parents() {
let temp = TempDir::new().unwrap();
let path = temp.path().join("a").join("b").join("c");
fs::create_dir_all(&path).unwrap();

let result = get_parents(&path, 2).unwrap();
assert_eq!(result, temp.path().join("a"));
}
}
Loading

0 comments on commit 922d7a8

Please sign in to comment.