Switches the database from SQL Server to MariaDB

The target Pi only has MariaDB, and SQL Server has no ARM64 build at
all - not a config problem, a real gap discovered during deployment
setup. Swapped the EF Core provider, regenerated every migration,
updated connection strings and the backup script everywhere they
appear.

Took two tries to land on a provider that actually works: Pomelo
builds fine against this project's EF Core 10 packages but fails at
runtime (it's compiled against 9's internal API surface, which moved
in 10 wherever Identity/DataProtection force the newer packages).
Oracle's official provider builds and migrates fine but has a real
MariaDB bug in its own migration-lock code, reproduced against a live
database. Kept Oracle's provider and worked around just that one
broken method - everything else it does is correct - rather than
give up more of the stack to chase a workaround.

Verified against a real local MariaDB end to end: all three
migrations applied, both hosts start clean, full suite still green.
This commit is contained in:
2026-07-29 11:59:09 +02:00
parent 579e0ceaac
commit 33d18ebbf8
33 changed files with 575 additions and 1367 deletions
@@ -20,8 +20,9 @@ assumes already exists — `deploy-scp.yaml` (U6) never creates any of it.
### 1.1 Prerequisites
- .NET 10 runtime installed on the Pi (ASM-03) — the publish is framework-dependent
(`infrastructure-design.md` § 1), so the Pi needs the runtime, not the full SDK
- `sqlcmd` installed, for the backup script (§ 4) — e.g. `mssql-tools18` / `unixodbc` on Debian-based
Raspberry Pi OS
- `mariadb-client` (or `mariadb-dump`/`mysqldump` specifically) installed, for the backup script (§ 4)
— already present on most Raspberry Pi OS images that also run `mariadb-server`; install
`mariadb-client` explicitly if the dump tool isn't already there
### 1.2 Account Model (revised — `webadmin` is not the deploy account)
@@ -106,7 +107,7 @@ below):
```ini
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://localhost:<port>
ConnectionStrings__DefaultConnection=Server=127.0.0.1,1433;User ID=<user>;Password=<password>;Database=SlpSoftware<Env>;TrustServerCertificate=True
ConnectionStrings__DefaultConnection=Server=127.0.0.1;Port=3306;Database=SlpSoftware<Env>;Uid=<user>;Pwd=<password>
JwtSettings__Secret=<secure-long-random-secret>
JwtSettings__Issuer=SlpModularCms
JwtSettings__Audience=SlpModularCmsPortal
@@ -213,14 +214,15 @@ touch ~/.config/slpsoftware-db-backup.env
chmod 600 ~/.config/slpsoftware-db-backup.env
```
```ini
DB_SERVER=127.0.0.1,1433
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=SlpSoftwareProduction
DB_USER=<a-login-with-backup-database-permission>
DB_USER=<a-login-with-just-SELECT-LOCK-TABLES-permission>
DB_PASSWORD=<password>
```
Kept **separate** from `shared/env` (§ 1.5) deliberately — the backup script needs its own
credential, ideally scoped to just `BACKUP DATABASE` permission rather than the application's own
data-access login.
credential, ideally scoped to just read access (`SELECT`, `LOCK TABLES` — everything `mariadb-dump`
needs) rather than the application's own data-access login.
### 1.9 Gitea Actions Variables and Secrets
Set once, in this repository's Gitea Actions settings. This is exactly where the real path lives —
@@ -286,22 +288,29 @@ if [[ ! -f "$CREDENTIALS_FILE" ]]; then
fi
# shellcheck source=/dev/null
source "$CREDENTIALS_FILE"
: "${DB_SERVER:?}" "${DB_NAME:?}" "${DB_USER:?}" "${DB_PASSWORD:?}"
: "${DB_HOST:?}" "${DB_PORT:?}" "${DB_NAME:?}" "${DB_USER:?}" "${DB_PASSWORD:?}"
BACKUP_DIR="$HOME/backups/slpsoftware/${ENVIRONMENT}"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date -u +%Y%m%d%H%M%S)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}-${TIMESTAMP}.bak"
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}-${TIMESTAMP}.sql.gz"
sqlcmd -S "$DB_SERVER" -U "$DB_USER" -P "$DB_PASSWORD" -C -Q \
"BACKUP DATABASE [$DB_NAME] TO DISK = N'$BACKUP_FILE' WITH INIT, COMPRESSION"
# --single-transaction: consistent snapshot without locking the tables for the whole dump duration
# (InnoDB only — every table here is, since that's EF Core's MySQL-provider default).
mariadb-dump \
-h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" \
--single-transaction --routines --triggers \
"$DB_NAME" | gzip > "$BACKUP_FILE"
echo "Backup written to $BACKUP_FILE"
# Retention: keep the 7 most recent backups for this environment
ls -1t "$BACKUP_DIR"/*.bak 2>/dev/null | tail -n +8 | xargs -r rm -f
ls -1t "$BACKUP_DIR"/*.sql.gz 2>/dev/null | tail -n +8 | xargs -r rm -f
```
`mariadb-dump` is MariaDB's own name for the tool (present since MariaDB 10.4-ish); if the host only
has the older `mysqldump` name, substitute it — same tool, same flags.
```bash
chmod +x ~/scripts/backup-slpmodularcms-db.sh
```
@@ -310,9 +319,13 @@ chmod +x ~/scripts/backup-slpmodularcms-db.sh
```bash
~/scripts/backup-slpmodularcms-db.sh production
```
Confirm a `.bak` file appears under `~/backups/slpsoftware/production/` and that `sqlcmd` didn't
silently fail (the script uses `set -euo pipefail`, so a real SQL error does propagate as a non-zero
exit — which fails the calling `deploy-scp.yaml` step, correctly blocking the deploy).
Confirm a `.sql.gz` file appears under `~/backups/slpsoftware/production/` and that the dump didn't
silently fail (the script uses `set -euo pipefail`, so a real error does propagate as a non-zero
exit — which fails the calling `deploy-scp.yaml` step, correctly blocking the deploy). Worth a
one-time restore rehearsal too — an untested backup is not a verified one:
```bash
gunzip -c ~/backups/slpsoftware/production/<file>.sql.gz | mariadb -h 127.0.0.1 -u root -p <a-scratch-database>
```
## 5. Future: Switching Production to FTPS (Shared Hosting)
@@ -336,9 +349,10 @@ than the transport:
`wwwroot/web/` persistence (FR-08, ASM-01) would need a **different** mechanism on such a host —
e.g. never touching a specific subfolder during upload, rather than linking a persistent directory
outside a swapped release tree, since "outside the release tree" may not be an available concept
- **Database backup** — shared hosting frequently does not expose direct `sqlcmd`/SSH access at all;
the backup step in `deploy-scp.yaml` (§ 4 script) would need to become either a provider-specific
API call or a documented manual pre-production step (the FR-20 fallback U6 already designed for)
- **Database backup** — shared hosting frequently does not expose direct `mariadb-dump`/SSH access
at all; the backup step in `deploy-scp.yaml` (§ 4 script) would need to become either a
provider-specific API call (e.g. a hosting-panel database backup feature) or a documented manual
pre-production step (the FR-20 fallback U6 already designed for)
### 5.3 What building `deploy-ftps.yaml` would actually require
1. A new reusable workflow implementing the **same five required inputs**
@@ -46,8 +46,7 @@ A backup is taken before every **production** deploy (`operations/deployment/dep
§ 4, invoked by `deploy-scp.yaml` when `run_db_backup: true`). To restore:
```bash
sqlcmd -S <server> -U <user> -P <password> -C -Q \
"RESTORE DATABASE [SlpModularCmsProduction] FROM DISK = N'<path-to-backup>.bak' WITH REPLACE"
gunzip -c <path-to-backup>.sql.gz | mariadb -h <server> -u <user> -p SlpSoftwareProduction
```
Restoring a database backup and rolling back the application release are **independent actions**