mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-29 05:52:08 -04:00
feat(migration): replace sqlx::migrate with standalone Go migrate tool
Schema is now managed by a separate Go CLI at tools/migrate/, which embeds
the SQL files copied from server/initialize/migrate/database/ and tracks
state in the shared schema_migrations table. Rust no longer owns migrations
at startup.
Rust startup:
1. Connect to DB
2. Probe for the `user` table (schema marker)
3. If missing, fork-exec `ppanel-migrate up` to bring schema to v2131
4. Create admin if not present
5. Continue with redis/queue/scheduler
Verified:
- Empty DB → ppanel-migrate applies all 39 migrations in ~0.4s, admin created
- Go server's ppanel_go DB → schema detected, migrate skipped, clean start
Changes:
- Add tools/migrate/ — Go module wrapping golang-migrate
* migrate/migrate.go: Migrate() + RunUp() (RunUp bypasses the iofs vs
os.ErrNotExist sentinel mismatch that breaks Up() on already-migrated DBs)
* migrate/sql/{postgres,mysql}/: SQL files copied from server
* cmd/migrate/main.go: CLI (up/down/version/force/drop)
- Widen password column from varchar(100) → varchar(255) in
00001_init_schema.up.sql to fit Rust's 112-char PBKDF2 hash
(Rust uses 16-byte salt; Go's 8-byte salt fits in 100 but Rust's doesn't)
- Rewrite src/migration.rs:
* Remove sqlx::migrate!, run_migrations()
* Add ensure_schema(db, cfg): probe + invoke ppanel-migrate subprocess
- Fix src/db.rs::build_dsn: previously used cfg.config verbatim even when
the default config string was for the wrong dialect (PG got MySQL's
charset=utf8mb4 string and crashed on connect)
- Delete old migrations/{postgres,mysql}/ (no longer used)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS `server`;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS `server_group`;
|
|
||||||
@@ -39,23 +39,20 @@ pub async fn init_pool(cfg: &DatabaseConfig) -> Result<Db, sqlx::Error> {
|
|||||||
|
|
||||||
// ─── DSN builder ─────────────────────────────────────────────────────────
|
// ─── DSN builder ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn build_dsn(cfg: &DatabaseConfig) -> String {
|
pub(crate) fn build_dsn(cfg: &DatabaseConfig) -> String {
|
||||||
|
let dialect = detect_dialect(cfg);
|
||||||
let addr = cfg
|
let addr = cfg
|
||||||
.addr
|
.addr
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.filter(|a| !a.is_empty())
|
.filter(|a| !a.is_empty())
|
||||||
.unwrap_or(match detect_dialect(cfg) {
|
.unwrap_or(match dialect {
|
||||||
Dialect::Postgres => "localhost:5432",
|
Dialect::Postgres => "localhost:5432",
|
||||||
Dialect::Mysql => "localhost:3306",
|
Dialect::Mysql => "localhost:3306",
|
||||||
});
|
});
|
||||||
let password = url_encode_password(&cfg.password);
|
let password = url_encode_password(&cfg.password);
|
||||||
let query = if cfg.config.is_empty() {
|
let query = pick_query(cfg, dialect);
|
||||||
default_query(cfg)
|
|
||||||
} else {
|
|
||||||
&cfg.config
|
|
||||||
};
|
|
||||||
|
|
||||||
match detect_dialect(cfg) {
|
match dialect {
|
||||||
Dialect::Postgres => format!(
|
Dialect::Postgres => format!(
|
||||||
"postgres://{}:{}@{}/{}?{}",
|
"postgres://{}:{}@{}/{}?{}",
|
||||||
cfg.username, password, addr, cfg.dbname, query,
|
cfg.username, password, addr, cfg.dbname, query,
|
||||||
@@ -71,8 +68,42 @@ fn detect_dialect(cfg: &DatabaseConfig) -> Dialect {
|
|||||||
Dialect::from_driver(&cfg.driver)
|
Dialect::from_driver(&cfg.driver)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_query(cfg: &DatabaseConfig) -> &'static str {
|
/// Pick the URL query string for the DSN.
|
||||||
match detect_dialect(cfg) {
|
///
|
||||||
|
/// `DatabaseConfig.config` defaults to a MySQL-flavoured string
|
||||||
|
/// (`charset=utf8mb4&parseTime=true&loc=...`), so an unset YAML field
|
||||||
|
/// silently gives the wrong params to a Postgres connection. To stay
|
||||||
|
/// backwards-compatible we only honour `cfg.config` when it looks
|
||||||
|
/// compatible with the active dialect; otherwise fall back to
|
||||||
|
/// `default_query`.
|
||||||
|
fn pick_query(cfg: &DatabaseConfig, dialect: Dialect) -> String {
|
||||||
|
let raw = cfg.config.trim();
|
||||||
|
if !raw.is_empty() && query_matches_dialect(raw, dialect) {
|
||||||
|
return raw.to_string();
|
||||||
|
}
|
||||||
|
default_query(dialect).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_matches_dialect(query: &str, dialect: Dialect) -> bool {
|
||||||
|
// Heuristic — MySQL uses `charset=` / `parseTime=` / `loc=`; Postgres
|
||||||
|
// uses `sslmode=` / `TimeZone=` / `channel_binding=`. Treat any MySQL-
|
||||||
|
// specific key under a Postgres dialect as a mismatch, and vice versa.
|
||||||
|
let q = query.to_ascii_lowercase();
|
||||||
|
match dialect {
|
||||||
|
Dialect::Postgres => {
|
||||||
|
!q.contains("charset=")
|
||||||
|
&& !q.contains("parsetime=")
|
||||||
|
&& !q.contains("&loc=")
|
||||||
|
&& !q.starts_with("loc=")
|
||||||
|
}
|
||||||
|
Dialect::Mysql => {
|
||||||
|
!(q.contains("sslmode=") || q.contains("timezone=") || q.contains("channel_binding="))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_query(dialect: Dialect) -> &'static str {
|
||||||
|
match dialect {
|
||||||
Dialect::Postgres => "sslmode=disable&TimeZone=Asia/Shanghai",
|
Dialect::Postgres => "sslmode=disable&TimeZone=Asia/Shanghai",
|
||||||
Dialect::Mysql => "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai",
|
Dialect::Mysql => "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai",
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-5
@@ -133,11 +133,8 @@ async fn main() {
|
|||||||
.expect("failed to connect to database");
|
.expect("failed to connect to database");
|
||||||
tracing::info!("database connected");
|
tracing::info!("database connected");
|
||||||
|
|
||||||
// ── Run pending migrations ──────────────────────────────────────────
|
// ── Ensure schema is present (invoke Go migrate tool if needed) ────
|
||||||
migration::run_migrations(&db)
|
migration::ensure_schema(&db, &cfg.database).await;
|
||||||
.await
|
|
||||||
.expect("database migration failed");
|
|
||||||
tracing::info!("database migrations applied");
|
|
||||||
|
|
||||||
// ── Seed initial admin account if needed ────────────────────────────
|
// ── Seed initial admin account if needed ────────────────────────────
|
||||||
migration::create_admin_user(&db, &cfg.administrator.email, &cfg.administrator.password)
|
migration::create_admin_user(&db, &cfg.administrator.email, &cfg.administrator.password)
|
||||||
|
|||||||
+152
-29
@@ -1,39 +1,168 @@
|
|||||||
//! Database migration runner and bootstrap utilities.
|
//! Database bootstrap utilities.
|
||||||
//!
|
//!
|
||||||
//! Embeds both MySQL and PostgreSQL migration files at compile time via
|
//! Schema migrations are **not** owned by this Rust binary. They are managed by
|
||||||
//! `sqlx::migrate!` and selects the correct set at runtime based on the
|
//! the standalone Go tool at `tools/migrate/` (built with `go build -o ppanel-migrate ./cmd/migrate`),
|
||||||
//! [`Dialect`] detected from configuration.
|
//! which uses golang-migrate and tracks state in the `schema_migrations` table
|
||||||
|
//! — the same table the Go server uses. That makes a single source of truth
|
||||||
|
//! for schema across the Go and Rust backends.
|
||||||
|
//!
|
||||||
|
//! On startup, this module probes for the presence of the `user` table. If it
|
||||||
|
//! is missing it shells out to the `ppanel-migrate` binary to bring the schema
|
||||||
|
//! up to date, then retries the probe. If the binary is not available or fails,
|
||||||
|
//! the process exits with a clear error message.
|
||||||
//!
|
//!
|
||||||
//! **NOTE**: This is the Rust rewrite of the Go backend. The Go version is
|
//! **NOTE**: This is the Rust rewrite of the Go backend. The Go version is
|
||||||
//! deprecated and will be replaced by this Rust implementation.
|
//! deprecated and will be replaced by this Rust implementation.
|
||||||
|
|
||||||
mod mysql_migrations {
|
|
||||||
#![allow(unused)]
|
|
||||||
pub(super) const MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("migrations/mysql");
|
|
||||||
}
|
|
||||||
|
|
||||||
mod postgres_migrations {
|
|
||||||
#![allow(unused)]
|
|
||||||
pub(super) fn migrator() -> sqlx::migrate::Migrator {
|
|
||||||
let mut m = sqlx::migrate!("migrations/postgres");
|
|
||||||
m.set_locking(false);
|
|
||||||
m.no_tx = true;
|
|
||||||
m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use crate::repository::Dialect;
|
pub use crate::repository::Dialect;
|
||||||
|
|
||||||
|
use crate::config::DatabaseConfig;
|
||||||
|
use crate::db::build_dsn;
|
||||||
use crate::repository::Db;
|
use crate::repository::Db;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
/// Run all pending migrations for the detected database dialect.
|
/// Find a probe table whose existence implies "the schema is at least migration 2131".
|
||||||
pub async fn run_migrations(db: &Db) -> Result<(), sqlx::migrate::MigrateError> {
|
///
|
||||||
|
/// `user` is created in `00001_init_schema.up.sql` and only ever dropped in
|
||||||
|
/// drop-style migrations that immediately re-create it (or are no-ops on top
|
||||||
|
/// of a fresh database), so its presence is a reliable schema-marker.
|
||||||
|
const SCHEMA_MARKER_TABLE: &str = "user";
|
||||||
|
|
||||||
|
/// Returns `Ok(true)` if the schema marker table is present and queryable,
|
||||||
|
/// `Ok(false)` if the table is missing, and `Err` for any other database error.
|
||||||
|
async fn schema_present(db: &Db) -> Result<bool, sqlx::Error> {
|
||||||
match db {
|
match db {
|
||||||
Db::Postgres(pool) => postgres_migrations::migrator().run(pool).await,
|
Db::Postgres(pool) => {
|
||||||
Db::Mysql(pool) => mysql_migrations::MIGRATOR.run(pool).await,
|
let row: Option<(bool,)> = sqlx::query_as(
|
||||||
|
r#"SELECT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name = $1
|
||||||
|
)"#,
|
||||||
|
)
|
||||||
|
.bind(SCHEMA_MARKER_TABLE)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(row.map(|(b,)| b).unwrap_or(false))
|
||||||
|
}
|
||||||
|
Db::Mysql(pool) => {
|
||||||
|
let row: Option<(i64,)> = sqlx::query_as(
|
||||||
|
r#"SELECT COUNT(*) FROM information_schema.tables
|
||||||
|
WHERE table_schema = DATABASE() AND table_name = ?"#,
|
||||||
|
)
|
||||||
|
.bind(SCHEMA_MARKER_TABLE)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(row.map(|(n,)| n > 0).unwrap_or(false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure the database schema is present, invoking the `ppanel-migrate` tool
|
||||||
|
/// if needed. Returns successfully if the schema is at (or beyond) the
|
||||||
|
/// tool's latest migration, otherwise exits the process with a clear error.
|
||||||
|
pub async fn ensure_schema(db: &Db, cfg: &DatabaseConfig) {
|
||||||
|
match schema_present(db).await {
|
||||||
|
Ok(true) => {
|
||||||
|
tracing::info!("schema marker table present, skipping migration tool");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"schema marker table `{}` not found — invoking ppanel-migrate to initialise the schema",
|
||||||
|
SCHEMA_MARKER_TABLE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("failed to probe schema marker: {e}");
|
||||||
|
// Don't bail out yet — the migration tool itself might still succeed
|
||||||
|
// (e.g. the database is reachable, just has no tables).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run_migrate_tool(cfg).await;
|
||||||
|
// Re-probe to confirm the schema is actually present now.
|
||||||
|
match schema_present(db).await {
|
||||||
|
Ok(true) => tracing::info!("schema initialised successfully"),
|
||||||
|
Ok(false) => panic!(
|
||||||
|
"ppanel-migrate ran but schema marker `{}` is still missing",
|
||||||
|
SCHEMA_MARKER_TABLE
|
||||||
|
),
|
||||||
|
Err(e) => panic!("schema marker probe failed after ppanel-migrate: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the path to the `ppanel-migrate` binary.
|
||||||
|
///
|
||||||
|
/// Search order:
|
||||||
|
/// 1. `${PPANEL_MIGRATE_BIN}` if set.
|
||||||
|
/// 2. Next to the current executable (`./ppanel-migrate`).
|
||||||
|
/// 3. One level up from the executable (cargo puts the binary in target/release/;
|
||||||
|
/// the migrate tool often lives at tools/migrate/ppanel-migrate).
|
||||||
|
/// 4. `$PATH` (via Command's default lookup).
|
||||||
|
fn locate_migrate_bin() -> PathBuf {
|
||||||
|
if let Ok(p) = std::env::var("PPANEL_MIGRATE_BIN") {
|
||||||
|
return PathBuf::from(p);
|
||||||
|
}
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
let candidate = dir.join("ppanel-migrate");
|
||||||
|
if candidate.is_file() {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if let Some(parent) = dir.parent() {
|
||||||
|
let candidate = parent.join("ppanel-migrate");
|
||||||
|
if candidate.is_file() {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathBuf::from("ppanel-migrate")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_migrate_tool(cfg: &DatabaseConfig) {
|
||||||
|
let bin = locate_migrate_bin();
|
||||||
|
let driver = match cfg.driver.as_str() {
|
||||||
|
"mysql" => "mysql",
|
||||||
|
_ => "postgres",
|
||||||
|
};
|
||||||
|
let dsn = build_dsn(cfg);
|
||||||
|
|
||||||
|
tracing::info!(driver, bin = %bin.display(), "running ppanel-migrate up");
|
||||||
|
|
||||||
|
// Run the migrate tool synchronously — it must complete before we serve
|
||||||
|
// any traffic. Failures are fatal.
|
||||||
|
let output = Command::new(&bin)
|
||||||
|
.arg("-driver")
|
||||||
|
.arg(driver)
|
||||||
|
.arg("-dsn")
|
||||||
|
.arg(&dsn)
|
||||||
|
.arg("up")
|
||||||
|
.output()
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"failed to execute ppanel-migrate at {}: {e}. \
|
||||||
|
Build it with `go build -o ppanel-migrate ./cmd/migrate` \
|
||||||
|
from ppanel-backend/tools/migrate, or set $PPANEL_MIGRATE_BIN",
|
||||||
|
bin.display(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
panic!(
|
||||||
|
"ppanel-migrate exited with status {}: {}{}{}",
|
||||||
|
output.status,
|
||||||
|
stdout,
|
||||||
|
if !stdout.is_empty() && !stderr.is_empty() { "\n" } else { "" },
|
||||||
|
stderr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tracing::info!("ppanel-migrate completed");
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// Bootstrap: initial admin account
|
// Bootstrap: initial admin account
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -64,7 +193,6 @@ async fn create_admin_user_pg(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = chrono::Utc::now().timestamp_millis();
|
|
||||||
let now_ts = chrono::Utc::now(); // chrono::DateTime<Utc> -> PG timestamp
|
let now_ts = chrono::Utc::now(); // chrono::DateTime<Utc> -> PG timestamp
|
||||||
let password_hash = hash_password_pbkdf2(password);
|
let password_hash = hash_password_pbkdf2(password);
|
||||||
let refer_code = generate_invite_code();
|
let refer_code = generate_invite_code();
|
||||||
@@ -193,8 +321,3 @@ fn hash_password_pbkdf2(password: &str) -> String {
|
|||||||
fn generate_invite_code() -> String {
|
fn generate_invite_code() -> String {
|
||||||
format!("u{}", &uuid::Uuid::new_v4().to_string().replace('-', "")[..12])
|
format!("u{}", &uuid::Uuid::new_v4().to_string().replace('-', "")[..12])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// Bootstrap: initial admin account
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
// ppanel-migrate is a standalone CLI for managing PPanel database schema.
|
||||||
|
//
|
||||||
|
// It uses the migrate package (copied from server/initialize/migrate) which embeds
|
||||||
|
// the SQL migration files and applies them via golang-migrate. Tracks state in the
|
||||||
|
// schema_migrations table — the same table the Go server uses — so the Rust
|
||||||
|
// ppanel-backend can attach to a database that was already initialised by either tool.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// ppanel-migrate -driver=postgres -dsn="postgres://..." up
|
||||||
|
// ppanel-migrate -driver=postgres -dsn="postgres://..." version
|
||||||
|
// ppanel-migrate -driver=postgres -dsn="postgres://..." force 2131
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
gomigrate "github.com/golang-migrate/migrate/v4"
|
||||||
|
|
||||||
|
ppmigrate "github.com/perfect-panel/ppanel-backend/tools/migrate/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
driver = flag.String("driver", "postgres", "Database driver: postgres | mysql")
|
||||||
|
dsn = flag.String("dsn", "", "Database DSN. URL scheme (postgres:// | mysql://) is auto-prepended if absent.")
|
||||||
|
verbose = flag.Bool("v", false, "Verbose logging")
|
||||||
|
)
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Fprintf(os.Stderr, "ppanel-migrate: standalone PPanel schema migration tool\n\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: ppanel-migrate -driver=<postgres|mysql> -dsn=<dsn> <command> [args]\n\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Commands:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " up [N] Apply all (or N) pending migrations\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " down [N] Roll back one (or N) migration\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " version Print current schema version\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " force <version> Mark database at <version> without running migrations\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " drop Drop every object in the database (dangerous)\n\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *dsn == "" {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
if *verbose {
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := flag.Arg(0)
|
||||||
|
sess := ppmigrate.Migrate(*driver, *dsn)
|
||||||
|
m := sess.Migrate
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "", "up":
|
||||||
|
if err := ppmigrate.RunUp(sess); err != nil && !errors.Is(err, gomigrate.ErrNoChange) {
|
||||||
|
log.Fatalf("up: %v", err)
|
||||||
|
}
|
||||||
|
reportVersion(m)
|
||||||
|
|
||||||
|
case "down":
|
||||||
|
n := -parseStep(flag.Arg(1))
|
||||||
|
if err := m.Steps(n); err != nil && !errors.Is(err, gomigrate.ErrNoChange) {
|
||||||
|
log.Fatalf("down: %v", err)
|
||||||
|
}
|
||||||
|
reportVersion(m)
|
||||||
|
|
||||||
|
case "version":
|
||||||
|
v, dirty, err := m.Version()
|
||||||
|
if errors.Is(err, gomigrate.ErrNilVersion) {
|
||||||
|
fmt.Println("no schema_migrations row (database is empty / not yet migrated)")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("version: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("version=%d dirty=%v\n", v, dirty)
|
||||||
|
|
||||||
|
case "force":
|
||||||
|
v, err := strconv.Atoi(flag.Arg(1))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("force: bad version %q: %v", flag.Arg(1), err)
|
||||||
|
}
|
||||||
|
if err := m.Force(v); err != nil {
|
||||||
|
log.Fatalf("force %d: %v", v, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("forced to version %d\n", v)
|
||||||
|
|
||||||
|
case "drop":
|
||||||
|
log.Println("WARNING: dropping all database objects")
|
||||||
|
if err := m.Drop(); err != nil {
|
||||||
|
log.Fatalf("drop: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("dropped")
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStep(s string) int {
|
||||||
|
if s == "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(s)
|
||||||
|
if err != nil || n <= 0 {
|
||||||
|
log.Fatalf("step must be a positive integer, got %q", s)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportVersion(m *gomigrate.Migrate) {
|
||||||
|
v, dirty, err := m.Version()
|
||||||
|
if errors.Is(err, gomigrate.ErrNilVersion) {
|
||||||
|
fmt.Println("no schema_migrations row (empty database)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("version: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("schema_migrations: version=%d dirty=%v\n", v, dirty)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module github.com/perfect-panel/ppanel-backend/tools/migrate
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||||
|
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||||
|
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package migrate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/mysql"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed sql/postgres/*.sql sql/mysql/*.sql
|
||||||
|
var sqlFiles embed.FS
|
||||||
|
|
||||||
|
// NoChange is re-exported so callers don't have to import golang-migrate directly.
|
||||||
|
var NoChange = migrate.ErrNoChange
|
||||||
|
|
||||||
|
// Session bundles a configured golang-migrate instance alongside the parsed
|
||||||
|
// source-version list so callers can introspect what migrations exist.
|
||||||
|
type Session struct {
|
||||||
|
Migrate *migrate.Migrate
|
||||||
|
Versions []uint // sorted ascending; all .up.sql versions found in source
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate returns a configured golang-migrate instance that reads SQL files from
|
||||||
|
// the embedded sql/{postgres,mysql}/ directories, based on the requested driver.
|
||||||
|
//
|
||||||
|
// driver: "postgres" or "mysql"
|
||||||
|
// dsn: golang-migrate URL (e.g. postgres://user:pass@host:port/db?sslmode=disable).
|
||||||
|
//
|
||||||
|
// If dsn does not include a URL scheme, the driver is prepended automatically.
|
||||||
|
func Migrate(driver, dsn string) *Session {
|
||||||
|
sourcePath := "sql/postgres"
|
||||||
|
databaseURL := dsn
|
||||||
|
switch driver {
|
||||||
|
case "mysql":
|
||||||
|
sourcePath = "sql/mysql"
|
||||||
|
databaseURL = ensureScheme("mysql://", dsn)
|
||||||
|
case "postgres":
|
||||||
|
databaseURL = ensureScheme("postgres://", dsn)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("[Migrate] unsupported database driver: %s", driver))
|
||||||
|
}
|
||||||
|
d, err := iofs.New(sqlFiles, sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("[Migrate] iofs.New error: %v", err))
|
||||||
|
}
|
||||||
|
client, err := migrate.NewWithSourceInstance("iofs", d, databaseURL)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("[Migrate] NewWithSourceInstance error: %v", err))
|
||||||
|
}
|
||||||
|
return &Session{
|
||||||
|
Migrate: client,
|
||||||
|
Versions: scanVersions(sourcePath),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceVersionRe matches migration filenames like "02131_xxx.up.sql" / ".down.sql".
|
||||||
|
var sourceVersionRe = regexp.MustCompile(`^([0-9]+)_[^.]+\.(up|down)\.sql$`)
|
||||||
|
|
||||||
|
// scanVersions lists all up-version numbers present in the embedded source dir.
|
||||||
|
func scanVersions(sourcePath string) []uint {
|
||||||
|
entries, err := sqlFiles.ReadDir(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("[scanVersions] read %s: %w", sourcePath, err))
|
||||||
|
}
|
||||||
|
seen := map[uint]struct{}{}
|
||||||
|
for _, e := range entries {
|
||||||
|
m := sourceVersionRe.FindStringSubmatch(e.Name())
|
||||||
|
if m == nil || m[2] != "up" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseUint(m[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[uint(v)] = struct{}{}
|
||||||
|
}
|
||||||
|
out := make([]uint, 0, len(seen))
|
||||||
|
for v := range seen {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunUp applies all pending migrations. Unlike m.Up(), it correctly handles the
|
||||||
|
// "database is already at the latest version" case under the iofs source driver —
|
||||||
|
// whose Next() / ReadUp() methods return fs.ErrNotExist rather than the
|
||||||
|
// os.ErrNotExist sentinel that golang-migrate's internal logic checks for.
|
||||||
|
//
|
||||||
|
// Returns migrate.ErrNoChange if there is nothing to apply.
|
||||||
|
func RunUp(s *Session) error {
|
||||||
|
if len(s.Versions) == 0 {
|
||||||
|
return fmt.Errorf("no migration files embedded")
|
||||||
|
}
|
||||||
|
srcLast := s.Versions[len(s.Versions)-1]
|
||||||
|
|
||||||
|
dbVer, _, err := s.Migrate.Version()
|
||||||
|
if errors.Is(err, migrate.ErrNilVersion) {
|
||||||
|
// Empty DB — apply everything from the top.
|
||||||
|
return s.Migrate.Up()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if uint(dbVer) >= srcLast {
|
||||||
|
// DB already at or beyond the latest source version. Nothing to do.
|
||||||
|
return migrate.ErrNoChange
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := int(srcLast - uint(dbVer))
|
||||||
|
return s.Migrate.Steps(steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureScheme(scheme, dsn string) string {
|
||||||
|
if strings.Contains(dsn, "://") {
|
||||||
|
return dsn
|
||||||
|
}
|
||||||
|
return scheme + dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep imports referenced (io/fs and os are used elsewhere via errors.Is)
|
||||||
|
var _ = fs.ErrNotExist
|
||||||
|
var _ = os.ErrNotExist
|
||||||
+1
-1
@@ -1,3 +1,4 @@
|
|||||||
|
-- 000001_init_schema.up.sql
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `ads`
|
CREATE TABLE IF NOT EXISTS `ads`
|
||||||
@@ -552,4 +553,3 @@ CREATE TABLE IF NOT EXISTS `server_rule_group`
|
|||||||
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
-- 000002_init_data.up.sql
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
-- auth_method
|
-- auth_method
|
||||||
-1
@@ -67,4 +67,3 @@ SET @sql = IF(@column_exists = 0,
|
|||||||
PREPARE stmt FROM @sql;
|
PREPARE stmt FROM @sql;
|
||||||
EXECUTE stmt;
|
EXECUTE stmt;
|
||||||
DEALLOCATE PREPARE stmt;
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
-1
@@ -15,4 +15,3 @@ CREATE TABLE IF NOT EXISTS `user_reset_subscribe_log`
|
|||||||
) ENGINE = InnoDB
|
) ENGINE = InnoDB
|
||||||
DEFAULT CHARSET = utf8mb4
|
DEFAULT CHARSET = utf8mb4
|
||||||
COLLATE = utf8mb4_general_ci;
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
@@ -21,4 +21,3 @@ CREATE TABLE `email_task` (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
-1
File diff suppressed because one or more lines are too long
@@ -26,4 +26,3 @@ CREATE TABLE IF NOT EXISTS `nodes` (
|
|||||||
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
-1
@@ -5,4 +5,3 @@ DROP COLUMN `server`,
|
|||||||
DROP COLUMN `server_group`;
|
DROP COLUMN `server_group`;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `server_rule_group`;
|
DROP TABLE IF EXISTS `server_rule_group`;
|
||||||
|
|
||||||
-1
@@ -5,4 +5,3 @@ ALTER TABLE `user`
|
|||||||
ADD COLUMN `only_first_purchase` TINYINT(1) NOT NULL DEFAULT 1
|
ADD COLUMN `only_first_purchase` TINYINT(1) NOT NULL DEFAULT 1
|
||||||
COMMENT 'Only First Purchase'
|
COMMENT 'Only First Purchase'
|
||||||
AFTER `referral_percentage`;
|
AFTER `referral_percentage`;
|
||||||
|
|
||||||
-1
@@ -1,2 +1 @@
|
|||||||
CREATE INDEX idx_traffic_log_time_user_sub ON traffic_log (timestamp, user_id, subscribe_id);
|
CREATE INDEX idx_traffic_log_time_user_sub ON traffic_log (timestamp, user_id, subscribe_id);
|
||||||
|
|
||||||
@@ -18,4 +18,3 @@ SET
|
|||||||
PREPARE stmt FROM @query;
|
PREPARE stmt FROM @query;
|
||||||
EXECUTE stmt;
|
EXECUTE stmt;
|
||||||
DEALLOCATE PREPARE stmt;
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
-1
@@ -33,4 +33,3 @@ SET @sql = (
|
|||||||
PREPARE stmt FROM @sql;
|
PREPARE stmt FROM @sql;
|
||||||
EXECUTE stmt;
|
EXECUTE stmt;
|
||||||
DEALLOCATE PREPARE stmt;
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
-1
@@ -5,4 +5,3 @@ SELECT 'site', 'CustomData', '{
|
|||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData'
|
SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData'
|
||||||
);
|
);
|
||||||
|
|
||||||
-1
@@ -1,2 +1 @@
|
|||||||
ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp);
|
ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp);
|
||||||
|
|
||||||
-1
@@ -2,4 +2,3 @@ ALTER TABLE `user_subscribe`
|
|||||||
ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT ''
|
ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT ''
|
||||||
COMMENT 'User note for subscription'
|
COMMENT 'User note for subscription'
|
||||||
AFTER `status`;
|
AFTER `status`;
|
||||||
|
|
||||||
-1
@@ -2,4 +2,3 @@ ALTER TABLE `user`
|
|||||||
ADD COLUMN `rules` TEXT NULL
|
ADD COLUMN `rules` TEXT NULL
|
||||||
COMMENT 'User rules for subscription'
|
COMMENT 'User rules for subscription'
|
||||||
AFTER `created_at`;
|
AFTER `created_at`;
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS `server`;
|
||||||
-1
@@ -1,3 +1,2 @@
|
|||||||
ALTER TABLE `subscribe`
|
ALTER TABLE `subscribe`
|
||||||
ADD COLUMN `show_original_price` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'display the original price: 0 not display, 1 display' AFTER `created_at`;
|
ADD COLUMN `show_original_price` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'display the original price: 0 not display, 1 display' AFTER `created_at`;
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS `server_group`;
|
||||||
-1
@@ -1,2 +1 @@
|
|||||||
CREATE INDEX idx_type_date ON system_logs (type, date);
|
CREATE INDEX idx_type_date ON system_logs (type, date);
|
||||||
|
|
||||||
-1
@@ -129,4 +129,3 @@ SET @sql = IF(@index_exists = 0,
|
|||||||
PREPARE stmt FROM @sql;
|
PREPARE stmt FROM @sql;
|
||||||
EXECUTE stmt;
|
EXECUTE stmt;
|
||||||
DEALLOCATE PREPARE stmt;
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
-1
@@ -13,4 +13,3 @@ CREATE TABLE IF NOT EXISTS `server_config_overrides`
|
|||||||
) ENGINE = InnoDB
|
) ENGINE = InnoDB
|
||||||
DEFAULT CHARSET = utf8mb4
|
DEFAULT CHARSET = utf8mb4
|
||||||
COLLATE = utf8mb4_general_ci;
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
-1
@@ -19,4 +19,3 @@ DEALLOCATE PREPARE stmt;
|
|||||||
UPDATE `payment`
|
UPDATE `payment`
|
||||||
SET `sort` = `id`
|
SET `sort` = `id`
|
||||||
WHERE `sort` = 0;
|
WHERE `sort` = 0;
|
||||||
|
|
||||||
-1
@@ -3,4 +3,3 @@ SELECT 'subscribe', 'ShowTutorial', 'true', 'bool', 'Show tutorial section on th
|
|||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM `system` WHERE `category` = 'subscribe' AND `key` = 'ShowTutorial'
|
SELECT 1 FROM `system` WHERE `category` = 'subscribe' AND `key` = 'ShowTutorial'
|
||||||
);
|
);
|
||||||
|
|
||||||
-1
@@ -2,4 +2,3 @@
|
|||||||
-- The Go code fix (serverPushStatusLogic.go, serverPushUserTrafficLogic.go)
|
-- The Go code fix (serverPushStatusLogic.go, serverPushUserTrafficLogic.go)
|
||||||
-- removing .UTC() is sufficient for MySQL environments.
|
-- removing .UTC() is sufficient for MySQL environments.
|
||||||
SELECT 1;
|
SELECT 1;
|
||||||
|
|
||||||
+1
-1
@@ -1,3 +1,4 @@
|
|||||||
|
-- 000001_init_schema.up.sql
|
||||||
CREATE TABLE IF NOT EXISTS "ads"
|
CREATE TABLE IF NOT EXISTS "ads"
|
||||||
(
|
(
|
||||||
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
@@ -455,4 +456,3 @@ CREATE TABLE IF NOT EXISTS "server_rule_group"
|
|||||||
PRIMARY KEY ("id"),
|
PRIMARY KEY ("id"),
|
||||||
CONSTRAINT "unique_name" UNIQUE ("name")
|
CONSTRAINT "unique_name" UNIQUE ("name")
|
||||||
);
|
);
|
||||||
|
|
||||||
+1
-1
@@ -1,3 +1,4 @@
|
|||||||
|
-- 000002_init_data.up.sql
|
||||||
-- auth_method
|
-- auth_method
|
||||||
INSERT INTO "auth_method" ("id", "method", "config", "enabled", "created_at", "updated_at")
|
INSERT INTO "auth_method" ("id", "method", "config", "enabled", "created_at", "updated_at")
|
||||||
VALUES
|
VALUES
|
||||||
@@ -113,4 +114,3 @@ VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-2
|
|||||||
SELECT setval(pg_get_serial_sequence('"auth_method"', 'id'), COALESCE((SELECT MAX("id") FROM "auth_method"), 1), true);
|
SELECT setval(pg_get_serial_sequence('"auth_method"', 'id'), COALESCE((SELECT MAX("id") FROM "auth_method"), 1), true);
|
||||||
SELECT setval(pg_get_serial_sequence('"subscribe_type"', 'id'), COALESCE((SELECT MAX("id") FROM "subscribe_type"), 1), true);
|
SELECT setval(pg_get_serial_sequence('"subscribe_type"', 'id'), COALESCE((SELECT MAX("id") FROM "subscribe_type"), 1), true);
|
||||||
SELECT setval(pg_get_serial_sequence('"system"', 'id'), COALESCE((SELECT MAX("id") FROM "system"), 1), true);
|
SELECT setval(pg_get_serial_sequence('"system"', 'id'), COALESCE((SELECT MAX("id") FROM "system"), 1), true);
|
||||||
|
|
||||||
-1
@@ -4,4 +4,3 @@ ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "platform" VARCHAR(100) NOT NULL
|
|||||||
ALTER TABLE "payment" DROP COLUMN IF EXISTS "mark";
|
ALTER TABLE "payment" DROP COLUMN IF EXISTS "mark";
|
||||||
ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "description" TEXT;
|
ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "description" TEXT;
|
||||||
ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "token" VARCHAR(255) DEFAULT NULL;
|
ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "token" VARCHAR(255) DEFAULT NULL;
|
||||||
|
|
||||||
-1
@@ -17,4 +17,3 @@ CREATE TABLE "server_rule_group"
|
|||||||
CONSTRAINT "uni_server_rule_group_name" UNIQUE ("name")
|
CONSTRAINT "uni_server_rule_group_name" UNIQUE ("name")
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS "server_rule_group_idx_enable" ON "server_rule_group" ("enable");
|
CREATE INDEX IF NOT EXISTS "server_rule_group_idx_enable" ON "server_rule_group" ("enable");
|
||||||
|
|
||||||
-1
@@ -13,4 +13,3 @@ CREATE TABLE IF NOT EXISTS "user_device_online_record"
|
|||||||
ALTER TABLE "user_subscribe" ADD COLUMN IF NOT EXISTS "finished_at" TIMESTAMP NULL;
|
ALTER TABLE "user_subscribe" ADD COLUMN IF NOT EXISTS "finished_at" TIMESTAMP NULL;
|
||||||
ALTER TABLE "application_config" ADD COLUMN IF NOT EXISTS "invitation_link" TEXT NULL DEFAULT NULL;
|
ALTER TABLE "application_config" ADD COLUMN IF NOT EXISTS "invitation_link" TEXT NULL DEFAULT NULL;
|
||||||
ALTER TABLE "application_config" ADD COLUMN IF NOT EXISTS "kr_website_id" VARCHAR(255) NULL DEFAULT NULL;
|
ALTER TABLE "application_config" ADD COLUMN IF NOT EXISTS "kr_website_id" VARCHAR(255) NULL DEFAULT NULL;
|
||||||
|
|
||||||
-1
@@ -13,4 +13,3 @@ CREATE TABLE IF NOT EXISTS "user_reset_subscribe_log"
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS "user_reset_subscribe_log_idx_user_id" ON "user_reset_subscribe_log" ("user_id");
|
CREATE INDEX IF NOT EXISTS "user_reset_subscribe_log_idx_user_id" ON "user_reset_subscribe_log" ("user_id");
|
||||||
CREATE INDEX IF NOT EXISTS "user_reset_subscribe_log_idx_user_subscribe_id" ON "user_reset_subscribe_log" ("user_subscribe_id");
|
CREATE INDEX IF NOT EXISTS "user_reset_subscribe_log_idx_user_subscribe_id" ON "user_reset_subscribe_log" ("user_subscribe_id");
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user