Migrations-Prüfsummen: Zeilenenden fixieren (v0.3.1)

Bestehende Installationen brachen beim Start mit dem Windows-Release-Binary
ab: "migration 1 was previously applied but has been modified" – obwohl die
Migration inhaltlich seit v0.2.0 unverändert ist.

Ursache: .gitattributes enthielt nur "* text=auto". Zusammen mit
core.autocrlf=true (Standard auf windows-latest-Runnern) checkt
actions/checkout die .sql-Dateien als CRLF aus. sqlx::migrate! bettet den
Dateiinhalt zur Compile-Zeit ein und bildet darüber eine SHA-384-Summe, die
beim Start gegen _sqlx_migrations geprüft wird:

  0001_initial.sql mit LF   -> 0651a39f...
  0001_initial.sql mit CRLF -> 90093a9a...

Die GitHub-Windows-Builds v0.2.0 und v0.3.0 tragen die CRLF-Variante, lokal
gebaute Exes und der Linux-Build die LF-Variante. Datenbanken, die mit einem
LF-Binary angelegt wurden, kollidieren daher mit dem Windows-Release-ZIP.

Zwei Änderungen:

1. .gitattributes erzwingt "*.sql text eol=lf". Ab jetzt erzeugen Windows-
   und Linux-Runner identische Prüfsummen.

2. db::repair_line_ending_checksums() hebt eine gespeicherte Prüfsumme vor
   dem Migrationslauf an – aber nur, wenn sie exakt der Zeilenenden-Variante
   desselben SQL entspricht, in beide Richtungen. Ohne diesen Schritt würde
   Punkt 1 jede Installation brechen, die mit dem CRLF-Binary aus dem
   GitHub-ZIP von v0.2.0/v0.3.0 angelegt wurde. Echte inhaltliche
   Abweichungen führen weiterhin zum Abbruch durch sqlx.

sha2 wird dafür direkte Dependency (war bereits transitiv gelockt, die
Auflösung ändert sich nicht).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Manuel
2026-08-19 09:51:50 +02:00
co-authored by Claude Opus 5
parent ae1b00f7df
commit b180b471bb
9 changed files with 99 additions and 11 deletions
+7
View File
@@ -1 +1,8 @@
* text=auto
# Migrationen MÜSSEN LF behalten: sqlx::migrate! bildet eine SHA-384-Prüfsumme
# über den Dateiinhalt und vergleicht sie mit der in _sqlx_migrations
# gespeicherten. Ein CRLF-Checkout (Windows-Buildrunner) erzeugt sonst eine
# andere Prüfsumme als ein LF-Checkout (Linux) -> "migration 1 was previously
# applied but has been modified" beim Start gegen eine bestehende Datenbank.
*.sql text eol=lf
+2 -2
View File
@@ -1,4 +1,4 @@
# easy-occp v0.3.0 – Installation & Erststart (Windows)
# easy-occp v0.3.1 – Installation & Erststart (Windows)
🌐 [English](INSTALL.md) · [Deutsch](ANLEITUNG.md) · [Français](INSTALL.fr.md) · [Español](INSTALL.es.md)
@@ -171,4 +171,4 @@ Dienst/Aufgabe entfernen, Ordner löschen — es gibt keine Registry-Einträge u
---
Version 0.3.0 · Lizenz: MIT
Version 0.3.1 · Lizenz: MIT
Generated
+2 -1
View File
@@ -578,7 +578,7 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "easy-occp"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"anyhow",
"argon2",
@@ -596,6 +596,7 @@ dependencies = [
"rust-embed",
"serde",
"serde_json",
"sha2",
"sqlx",
"thiserror 2.0.18",
"tokio",
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "easy-occp"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
rust-version = "1.80"
description = "Einfaches Management-Tool für Wallboxen (OCPP 1.5/1.6/2.0.1) für KMUs"
@@ -30,6 +30,7 @@ clap = { version = "4", features = ["derive"] }
futures-util = "0.3"
parking_lot = "0.12"
base64 = "0.22"
sha2 = "0.10"
dashmap = "6"
printpdf = "0.7"
+2 -2
View File
@@ -1,4 +1,4 @@
# easy-occp v0.3.0 – Instalación y primer arranque (Windows)
# easy-occp v0.3.1 – Instalación y primer arranque (Windows)
🌐 [English](INSTALL.md) · [Deutsch](ANLEITUNG.md) · [Français](INSTALL.fr.md) · [Español](INSTALL.es.md)
@@ -175,4 +175,4 @@ Elimine el servicio/la tarea y borre la carpeta — no hay entradas en el regist
---
Versión 0.3.0 · Licencia: MIT
Versión 0.3.1 · Licencia: MIT
+2 -2
View File
@@ -1,4 +1,4 @@
# easy-occp v0.3.0 – Installation & premier démarrage (Windows)
# easy-occp v0.3.1 – Installation & premier démarrage (Windows)
🌐 [English](INSTALL.md) · [Deutsch](ANLEITUNG.md) · [Français](INSTALL.fr.md) · [Español](INSTALL.es.md)
@@ -174,4 +174,4 @@ Supprimez le service/la tâche, supprimez le dossier — il n'y a ni entrées de
---
Version 0.3.0 · Licence : MIT
Version 0.3.1 · Licence : MIT
+2 -2
View File
@@ -1,4 +1,4 @@
# easy-occp v0.3.0 – Installation & First Start (Windows)
# easy-occp v0.3.1 – Installation & First Start (Windows)
🌐 [English](INSTALL.md) · [Deutsch](ANLEITUNG.md) · [Français](INSTALL.fr.md) · [Español](INSTALL.es.md)
@@ -172,4 +172,4 @@ Remove the service/task, delete the folder — there are no registry entries and
---
Version 0.3.0 · License: MIT
Version 0.3.1 · License: MIT
+77
View File
@@ -85,3 +85,80 @@ pub async fn setting_set(db: &SqlitePool, key: &str, value: &str) -> Result<()>
.await?;
Ok(())
}
/// Repariert Prüfsummen in `_sqlx_migrations`, die sich nur durch die
/// Zeilenenden der Migrationsdatei unterscheiden.
///
/// Hintergrund: `sqlx::migrate!` bettet den Dateiinhalt zur Compile-Zeit ein und
/// prüft beim Start eine SHA-384-Summe darüber. Wird dieselbe Migration einmal
/// aus einem LF- und einmal aus einem CRLF-Checkout gebaut (Windows-Buildrunner
/// ohne `eol=lf`), unterscheiden sich die Summen und sqlx bricht mit
/// "migration N was previously applied but has been modified" ab, obwohl das
/// SQL identisch ist. In diesem – und nur in diesem – Fall wird die gespeicherte
/// Prüfsumme auf den aktuellen Wert gehoben.
pub async fn repair_line_ending_checksums(
db: &SqlitePool,
migrator: &sqlx::migrate::Migrator,
) -> Result<()> {
let table: Option<(String,)> = sqlx::query_as(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = '_sqlx_migrations'",
)
.fetch_optional(db)
.await
.context("Migrationstabelle konnte nicht gelesen werden")?;
if table.is_none() {
return Ok(());
}
let applied: Vec<(i64, Vec<u8>)> =
sqlx::query_as("SELECT version, checksum FROM _sqlx_migrations")
.fetch_all(db)
.await
.context("Angewandte Migrationen konnten nicht gelesen werden")?;
for migration in migrator.iter() {
let Some((_, stored)) = applied.iter().find(|(v, _)| *v == migration.version) else {
continue;
};
if stored.as_slice() == migration.checksum.as_ref() {
continue;
}
// Gegenstück mit umgedrehten Zeilenenden bilden und prüfen.
let normalized = migration.sql.replace("\r\n", "\n");
let variant = if migration.sql.contains("\r\n") {
normalized
} else {
normalized.replace('\n', "\r\n")
};
if stored.as_slice() != sha384(variant.as_bytes()).as_slice() {
continue; // Echte inhaltliche Abweichung – sqlx soll abbrechen.
}
sqlx::query("UPDATE _sqlx_migrations SET checksum = ?1 WHERE version = ?2")
.bind(migration.checksum.as_ref())
.bind(migration.version)
.execute(db)
.await
.with_context(|| {
format!(
"Prüfsumme der Migration {} konnte nicht aktualisiert werden",
migration.version
)
})?;
tracing::warn!(
"Migration {} ({}): Prüfsumme wich nur in den Zeilenenden ab und wurde korrigiert.",
migration.version,
migration.description
);
}
Ok(())
}
fn sha384(data: &[u8]) -> Vec<u8> {
use sha2::{Digest, Sha384};
let mut hasher = Sha384::new();
hasher.update(data);
hasher.finalize().to_vec()
}
+3 -1
View File
@@ -65,7 +65,9 @@ async fn main() -> Result<()> {
.await
.context("SQLite-Pool konnte nicht geöffnet werden")?;
sqlx::migrate!("./migrations")
let migrator = sqlx::migrate!("./migrations");
db::repair_line_ending_checksums(&db, &migrator).await?;
migrator
.run(&db)
.await
.context("Migrationen fehlgeschlagen")?;