mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-29 05:52:08 -04:00
feat(migration)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
//! Cross-language interface definition for the migration tool.
|
||||
//!
|
||||
//! This file is the **single source of truth** for the Rust↔Go boundary.
|
||||
//! rust2go reads it and auto-generates:
|
||||
//! - `gen.go` (Go-side interface + CGO exports) at
|
||||
//! `tools/migrate/migrate/gen.go`
|
||||
//! - The C binding file (`_go_bindings.rs` by default) used by the
|
||||
//! Rust crate to call into the Go static library.
|
||||
//!
|
||||
//! The hand-written Go side lives in `tools/migrate/migrate/impl.go`.
|
||||
|
||||
pub mod binding {
|
||||
#![allow(warnings)]
|
||||
rust2go::r2g_include_binding!();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Wire types — must be `#[derive(R2G, Clone)]` and use only path-local
|
||||
// primitive types. See the rust2go skill notes for the supported type
|
||||
// matrix.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Configuration for a single migration invocation. Maps 1:1 to the
|
||||
/// `-driver` + `-dsn` flags on the standalone CLI.
|
||||
#[derive(rust2go::R2G, Clone)]
|
||||
pub struct MigrateConfig {
|
||||
/// Either `"postgres"` or `"mysql"`. Any other value causes the Go
|
||||
/// side to return an error in `MigrateOutcome.error`.
|
||||
pub driver: String,
|
||||
|
||||
/// Full DSN understood by golang-migrate. The Go side will
|
||||
/// auto-prepend `postgres://` or `mysql://` if missing, mirroring
|
||||
/// the CLI behaviour.
|
||||
pub dsn: String,
|
||||
}
|
||||
|
||||
/// Result of a migration call. Errors are signalled by populating
|
||||
/// `error` with a non-empty message — we deliberately use a plain
|
||||
/// `String` (not a sum type) so the type marshalling stays trivial and
|
||||
/// panic-free across the FFI boundary.
|
||||
#[derive(rust2go::R2G, Clone)]
|
||||
pub struct MigrateOutcome {
|
||||
/// Final schema version after the call. `0` if the
|
||||
/// `schema_migrations` table is empty (fresh database).
|
||||
pub version: u32,
|
||||
|
||||
/// Whether golang-migrate flagged the migration as dirty. A dirty
|
||||
/// state means a previous migration failed mid-way; the caller
|
||||
/// should refuse to serve traffic and request human intervention.
|
||||
pub dirty: bool,
|
||||
|
||||
/// Empty string on success. Populated with the Go-side error
|
||||
/// message on failure. Rust converts this to a panic with the
|
||||
/// same semantics as the previous subprocess call.
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Trait — the cross-language service definition.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Schema migration service exposed from Go to Rust.
|
||||
///
|
||||
/// Each method is a **synchronous** call: Rust blocks the calling
|
||||
/// thread until Go returns. This is intentional — these are called
|
||||
/// once at startup before any traffic is served, so we don't need
|
||||
/// async / shm / drop-safe machinery (which would add complexity for
|
||||
/// no runtime benefit). The Go side is a single goroutine per call,
|
||||
/// gated by a mutex on the `init` registration.
|
||||
#[rust2go::r2g]
|
||||
pub trait MigrateService {
|
||||
/// Apply all pending migrations. Equivalent to `ppanel-migrate up`.
|
||||
/// Idempotent: returns success with `version = latest` if the
|
||||
/// database is already at the latest source version (matches the
|
||||
/// `ErrNoChange` semantics of the standalone CLI).
|
||||
fn up(cfg: MigrateConfig) -> MigrateOutcome;
|
||||
|
||||
/// Read the current `schema_migrations.version` and `dirty` flag
|
||||
/// without applying anything. Useful for ops/debug.
|
||||
fn version(cfg: MigrateConfig) -> MigrateOutcome;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Rust binding to the Go migration tool (`tools/migrate/migrate`).
|
||||
//!
|
||||
//! This crate wraps the Go-based schema migrator behind a Rust API
|
||||
//! that can be called directly from the main binary — no subprocess,
|
||||
//! no out-of-band `ppanel-migrate` binary, no `PPANEL_MIGRATE_BIN`
|
||||
//! resolution.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌────────────────────────┐ FFI ┌──────────────────────┐
|
||||
//! │ ppanel-backend (Rust) │ ───────────────► │ Go staticlib │
|
||||
//! │ - main.rs │ │ - golang-migrate │
|
||||
//! │ - migration.rs │ │ - SQL files │
|
||||
//! │ │ │ (embedded via │
|
||||
//! │ │ │ //go:embed) │
|
||||
//! └────────────────────────┘ └──────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! The Go side opens its own short-lived database connection per
|
||||
//! call. The connection is closed when the call returns. We do not
|
||||
//! share sqlx pools across the FFI boundary — that would require
|
||||
//! additional plumbing for no real benefit (the migrator only runs
|
||||
//! at startup).
|
||||
//!
|
||||
//! # Build
|
||||
//!
|
||||
//! The build is driven by `build.rs`, which calls
|
||||
//! `rust2go::Builder`. The Builder compiles the Go code as CGO and
|
||||
//! produces `_go_bindings.rs` containing the C declarations used by
|
||||
//! the Rust side. The Go glue (`gen.go`) is regenerated automatically
|
||||
//! whenever `src/idl.rs` changes.
|
||||
//!
|
||||
//! # Runtime
|
||||
//!
|
||||
//! The user must set the env vars documented by rust2go when the
|
||||
//! binary is launched:
|
||||
//!
|
||||
//! ```text
|
||||
//! GODEBUG=invalidptr=0,cgocheck=0
|
||||
//! ```
|
||||
//!
|
||||
//! Without these, Go's conservative GC pointer checks will flag the
|
||||
//! FFI references as invalid and abort. This is a property of
|
||||
//! rust2go, not specific to this crate.
|
||||
|
||||
pub mod idl;
|
||||
|
||||
pub use idl::{MigrateConfig, MigrateOutcome, MigrateService, MigrateServiceImpl};
|
||||
|
||||
/// Apply all pending schema migrations.
|
||||
///
|
||||
/// Mirrors the behaviour of the previous `ppanel-migrate up`
|
||||
/// subprocess call:
|
||||
/// - **Idempotent**: if the database is already at the latest
|
||||
/// version, returns silently with the current version.
|
||||
/// - **Fail-fast**: on any Go-side error, panics with the Go error
|
||||
/// message. The migrator is called at startup before any traffic
|
||||
/// is served, so failing the process is the correct behaviour.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `driver`: `"postgres"` or `"mysql"`.
|
||||
/// - `dsn`: connection string in golang-migrate URL form. The Go
|
||||
/// side will auto-prepend the URL scheme if missing.
|
||||
pub fn up(driver: &str, dsn: &str) -> MigrateOutcome {
|
||||
let cfg = MigrateConfig {
|
||||
driver: driver.to_string(),
|
||||
dsn: dsn.to_string(),
|
||||
};
|
||||
let outcome = MigrateServiceImpl::up(cfg);
|
||||
if !outcome.error.is_empty() {
|
||||
panic!(
|
||||
"ppanel-migrate up failed: {}\n\
|
||||
Build via: `cd tools/migrate && go build -o ppanel-migrate ./cmd/migrate`\n\
|
||||
(or rely on the rust2go FFI path embedded at build time).",
|
||||
outcome.error
|
||||
);
|
||||
}
|
||||
outcome
|
||||
}
|
||||
|
||||
/// Read the current schema version without applying anything.
|
||||
///
|
||||
/// Returns `MigrateOutcome { version: 0, dirty: false, error: "" }`
|
||||
/// for a fresh database (no `schema_migrations` rows).
|
||||
pub fn version(driver: &str, dsn: &str) -> MigrateOutcome {
|
||||
let cfg = MigrateConfig {
|
||||
driver: driver.to_string(),
|
||||
dsn: dsn.to_string(),
|
||||
};
|
||||
MigrateServiceImpl::version(cfg)
|
||||
}
|
||||
Reference in New Issue
Block a user